style: apply black and isort formatting across entire codebase
- Standardize quote style (single to double quotes) - Reorder and group imports alphabetically - Fix line breaks and indentation for consistency - Apply PEP 8 formatting standards Also updated Makefile to exclude both venv and .venv from code quality checks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
193
app/api/deps.py
193
app/api/deps.py
@@ -34,22 +34,20 @@ The cookie path restrictions prevent cross-context cookie leakage:
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, Request, Cookie
|
||||
from fastapi import Cookie, Depends, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import (AdminRequiredException,
|
||||
InsufficientPermissionsException,
|
||||
InvalidTokenException,
|
||||
UnauthorizedVendorAccessException,
|
||||
VendorNotFoundException)
|
||||
from middleware.auth import AuthManager
|
||||
from middleware.rate_limiter import RateLimiter
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.user import User
|
||||
from app.exceptions import (
|
||||
AdminRequiredException,
|
||||
InvalidTokenException,
|
||||
InsufficientPermissionsException,
|
||||
VendorNotFoundException,
|
||||
UnauthorizedVendorAccessException
|
||||
)
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
# Initialize dependencies
|
||||
security = HTTPBearer(auto_error=False) # auto_error=False prevents automatic 403
|
||||
@@ -62,11 +60,12 @@ logger = logging.getLogger(__name__)
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _get_token_from_request(
|
||||
credentials: Optional[HTTPAuthorizationCredentials],
|
||||
cookie_value: Optional[str],
|
||||
cookie_name: str,
|
||||
request_path: str
|
||||
credentials: Optional[HTTPAuthorizationCredentials],
|
||||
cookie_value: Optional[str],
|
||||
cookie_name: str,
|
||||
request_path: str,
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
Extract token from Authorization header or cookie.
|
||||
@@ -108,10 +107,7 @@ def _validate_user_token(token: str, db: Session) -> User:
|
||||
Raises:
|
||||
InvalidTokenException: If token is invalid
|
||||
"""
|
||||
mock_credentials = HTTPAuthorizationCredentials(
|
||||
scheme="Bearer",
|
||||
credentials=token
|
||||
)
|
||||
mock_credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
|
||||
return auth_manager.get_current_user(db, mock_credentials)
|
||||
|
||||
|
||||
@@ -119,11 +115,12 @@ def _validate_user_token(token: str, db: Session) -> User:
|
||||
# ADMIN AUTHENTICATION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_current_admin_from_cookie_or_header(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
admin_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
admin_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current admin user from admin_token cookie or Authorization header.
|
||||
@@ -148,10 +145,7 @@ def get_current_admin_from_cookie_or_header(
|
||||
AdminRequiredException: If user is not admin
|
||||
"""
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
admin_token,
|
||||
"admin_token",
|
||||
str(request.url.path)
|
||||
credentials, admin_token, "admin_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -172,8 +166,8 @@ def get_current_admin_from_cookie_or_header(
|
||||
|
||||
|
||||
def get_current_admin_api(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current admin user from Authorization header ONLY.
|
||||
@@ -208,11 +202,12 @@ def get_current_admin_api(
|
||||
# VENDOR AUTHENTICATION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_current_vendor_from_cookie_or_header(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
vendor_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
vendor_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current vendor user from vendor_token cookie or Authorization header.
|
||||
@@ -237,10 +232,7 @@ def get_current_vendor_from_cookie_or_header(
|
||||
InsufficientPermissionsException: If user is not vendor or is admin
|
||||
"""
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
vendor_token,
|
||||
"vendor_token",
|
||||
str(request.url.path)
|
||||
credentials, vendor_token, "vendor_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -270,8 +262,8 @@ def get_current_vendor_from_cookie_or_header(
|
||||
|
||||
|
||||
def get_current_vendor_api(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current vendor user from Authorization header ONLY.
|
||||
@@ -310,11 +302,12 @@ def get_current_vendor_api(
|
||||
# CUSTOMER AUTHENTICATION (SHOP)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_current_customer_from_cookie_or_header(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
customer_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
customer_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get current customer from customer_token cookie or Authorization header.
|
||||
@@ -338,15 +331,14 @@ def get_current_customer_from_cookie_or_header(
|
||||
Raises:
|
||||
InvalidTokenException: If no token or invalid token
|
||||
"""
|
||||
from models.database.customer import Customer
|
||||
from jose import jwt, JWTError
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from jose import JWTError, jwt
|
||||
|
||||
from models.database.customer import Customer
|
||||
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
customer_token,
|
||||
"customer_token",
|
||||
str(request.url.path)
|
||||
credentials, customer_token, "customer_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -356,9 +348,7 @@ def get_current_customer_from_cookie_or_header(
|
||||
# Decode and validate customer JWT token
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
auth_manager.secret_key,
|
||||
algorithms=[auth_manager.algorithm]
|
||||
token, auth_manager.secret_key, algorithms=[auth_manager.algorithm]
|
||||
)
|
||||
|
||||
# Verify this is a customer token
|
||||
@@ -375,7 +365,9 @@ def get_current_customer_from_cookie_or_header(
|
||||
|
||||
# Verify token hasn't expired
|
||||
exp = payload.get("exp")
|
||||
if exp and datetime.fromtimestamp(exp, tz=timezone.utc) < datetime.now(timezone.utc):
|
||||
if exp and datetime.fromtimestamp(exp, tz=timezone.utc) < datetime.now(
|
||||
timezone.utc
|
||||
):
|
||||
logger.warning(f"Expired customer token for customer_id={customer_id}")
|
||||
raise InvalidTokenException("Token has expired")
|
||||
|
||||
@@ -400,8 +392,8 @@ def get_current_customer_from_cookie_or_header(
|
||||
|
||||
|
||||
def get_current_customer_api(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current customer user from Authorization header ONLY.
|
||||
@@ -445,9 +437,10 @@ def get_current_customer_api(
|
||||
# GENERIC AUTHENTICATION (for mixed-use endpoints)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current authenticated user from Authorization header only.
|
||||
@@ -475,10 +468,11 @@ def get_current_user(
|
||||
# VENDOR OWNERSHIP VERIFICATION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_user_vendor(
|
||||
vendor_code: str,
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
vendor_code: str,
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Vendor:
|
||||
"""
|
||||
Get vendor and verify user ownership/membership.
|
||||
@@ -500,9 +494,7 @@ def get_user_vendor(
|
||||
VendorNotFoundException: If vendor doesn't exist
|
||||
UnauthorizedVendorAccessException: If user doesn't have access
|
||||
"""
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.vendor_code == vendor_code.upper()
|
||||
).first()
|
||||
vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code.upper()).first()
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(vendor_code)
|
||||
@@ -517,10 +509,12 @@ def get_user_vendor(
|
||||
# User doesn't have access to this vendor
|
||||
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PERMISSIONS CHECKING
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def require_vendor_permission(permission: str):
|
||||
"""
|
||||
Dependency factory to require a specific vendor permission.
|
||||
@@ -535,9 +529,9 @@ def require_vendor_permission(permission: str):
|
||||
"""
|
||||
|
||||
def permission_checker(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
) -> User:
|
||||
# Get vendor from request state (set by middleware)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
@@ -557,9 +551,9 @@ def require_vendor_permission(permission: str):
|
||||
|
||||
|
||||
def require_vendor_owner(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
) -> User:
|
||||
"""
|
||||
Dependency to require vendor owner role.
|
||||
@@ -600,9 +594,9 @@ def require_any_vendor_permission(*permissions: str):
|
||||
"""
|
||||
|
||||
def permission_checker(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
) -> User:
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
if not vendor:
|
||||
@@ -610,8 +604,7 @@ def require_any_vendor_permission(*permissions: str):
|
||||
|
||||
# Check if user has ANY of the required permissions
|
||||
has_permission = any(
|
||||
current_user.has_vendor_permission(vendor.id, perm)
|
||||
for perm in permissions
|
||||
current_user.has_vendor_permission(vendor.id, perm) for perm in permissions
|
||||
)
|
||||
|
||||
if not has_permission:
|
||||
@@ -641,9 +634,9 @@ def require_all_vendor_permissions(*permissions: str):
|
||||
"""
|
||||
|
||||
def permission_checker(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
) -> User:
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
if not vendor:
|
||||
@@ -651,7 +644,8 @@ def require_all_vendor_permissions(*permissions: str):
|
||||
|
||||
# Check if user has ALL required permissions
|
||||
missing_permissions = [
|
||||
perm for perm in permissions
|
||||
perm
|
||||
for perm in permissions
|
||||
if not current_user.has_vendor_permission(vendor.id, perm)
|
||||
]
|
||||
|
||||
@@ -667,8 +661,8 @@ def require_all_vendor_permissions(*permissions: str):
|
||||
|
||||
|
||||
def get_user_permissions(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
) -> list:
|
||||
"""
|
||||
Get all permissions for current user in current vendor.
|
||||
@@ -682,6 +676,7 @@ def get_user_permissions(
|
||||
# If owner, return all permissions
|
||||
if current_user.is_owner_of(vendor.id):
|
||||
from app.core.permissions import VendorPermissions
|
||||
|
||||
return [p.value for p in VendorPermissions]
|
||||
|
||||
# Get permissions from vendor membership
|
||||
@@ -696,11 +691,12 @@ def get_user_permissions(
|
||||
# OPTIONAL AUTHENTICATION (For Login Page Redirects)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_current_admin_optional(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
admin_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
admin_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Get current admin user from admin_token cookie or Authorization header.
|
||||
@@ -723,10 +719,7 @@ def get_current_admin_optional(
|
||||
None: If no token, invalid token, or user is not admin
|
||||
"""
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
admin_token,
|
||||
"admin_token",
|
||||
str(request.url.path)
|
||||
credentials, admin_token, "admin_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -747,10 +740,10 @@ def get_current_admin_optional(
|
||||
|
||||
|
||||
def get_current_vendor_optional(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
vendor_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
vendor_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Get current vendor user from vendor_token cookie or Authorization header.
|
||||
@@ -773,10 +766,7 @@ def get_current_vendor_optional(
|
||||
None: If no token, invalid token, or user is not vendor
|
||||
"""
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
vendor_token,
|
||||
"vendor_token",
|
||||
str(request.url.path)
|
||||
credentials, vendor_token, "vendor_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -797,10 +787,10 @@ def get_current_vendor_optional(
|
||||
|
||||
|
||||
def get_current_customer_optional(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
customer_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
customer_token: Optional[str] = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Get current customer user from customer_token cookie or Authorization header.
|
||||
@@ -823,10 +813,7 @@ def get_current_customer_optional(
|
||||
None: If no token, invalid token, or user is not customer
|
||||
"""
|
||||
token, source = _get_token_from_request(
|
||||
credentials,
|
||||
customer_token,
|
||||
"customer_token",
|
||||
str(request.url.path)
|
||||
credentials, customer_token, "customer_token", str(request.url.path)
|
||||
)
|
||||
|
||||
if not token:
|
||||
@@ -844,5 +831,3 @@ def get_current_customer_optional(
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ This module provides:
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1 import admin, vendor, shop
|
||||
|
||||
from app.api.v1 import admin, shop, vendor
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -18,31 +19,18 @@ api_router = APIRouter()
|
||||
# Prefix: /api/v1/admin
|
||||
# ============================================================================
|
||||
|
||||
api_router.include_router(
|
||||
admin.router,
|
||||
prefix="/v1/admin",
|
||||
tags=["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"]
|
||||
)
|
||||
api_router.include_router(vendor.router, prefix="/v1/vendor", tags=["vendor"])
|
||||
|
||||
# ============================================================================
|
||||
# SHOP ROUTES (Public shop frontend API)
|
||||
# Prefix: /api/v1/shop
|
||||
# ============================================================================
|
||||
|
||||
api_router.include_router(
|
||||
shop.router,
|
||||
prefix="/v1/shop",
|
||||
tags=["shop"]
|
||||
)
|
||||
|
||||
api_router.include_router(shop.router, prefix="/v1/shop", tags=["shop"])
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
API Version 1 - All endpoints
|
||||
"""
|
||||
|
||||
from . import admin, vendor, shop
|
||||
from . import admin, shop, vendor
|
||||
|
||||
__all__ = ["admin", "vendor", "shop"]
|
||||
__all__ = ["admin", "vendor", "shop"]
|
||||
|
||||
@@ -24,21 +24,9 @@ IMPORTANT:
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Import all admin routers
|
||||
from . import (
|
||||
auth,
|
||||
vendors,
|
||||
vendor_domains,
|
||||
vendor_themes,
|
||||
users,
|
||||
dashboard,
|
||||
marketplace,
|
||||
monitoring,
|
||||
audit,
|
||||
settings,
|
||||
notifications,
|
||||
content_pages,
|
||||
code_quality
|
||||
)
|
||||
from . import (audit, auth, code_quality, content_pages, dashboard,
|
||||
marketplace, monitoring, notifications, settings, users,
|
||||
vendor_domains, vendor_themes, vendors)
|
||||
|
||||
# Create admin router
|
||||
router = APIRouter()
|
||||
@@ -66,7 +54,9 @@ router.include_router(vendor_domains.router, tags=["admin-vendor-domains"])
|
||||
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
|
||||
|
||||
# Include content pages management endpoints
|
||||
router.include_router(content_pages.router, prefix="/content-pages", tags=["admin-content-pages"])
|
||||
router.include_router(
|
||||
content_pages.router, prefix="/content-pages", tags=["admin-content-pages"]
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -115,7 +105,9 @@ router.include_router(notifications.router, tags=["admin-notifications"])
|
||||
# ============================================================================
|
||||
|
||||
# Include code quality and architecture validation endpoints
|
||||
router.include_router(code_quality.router, prefix="/code-quality", tags=["admin-code-quality"])
|
||||
router.include_router(
|
||||
code_quality.router, prefix="/code-quality", tags=["admin-code-quality"]
|
||||
)
|
||||
|
||||
# Export the router
|
||||
__all__ = ["router"]
|
||||
|
||||
@@ -9,8 +9,8 @@ Provides endpoints for:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -18,12 +18,10 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
from models.schema.admin import (
|
||||
AdminAuditLogResponse,
|
||||
AdminAuditLogFilters,
|
||||
AdminAuditLogListResponse
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.schema.admin import (AdminAuditLogFilters,
|
||||
AdminAuditLogListResponse,
|
||||
AdminAuditLogResponse)
|
||||
|
||||
router = APIRouter(prefix="/audit")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,15 +29,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/logs", response_model=AdminAuditLogListResponse)
|
||||
def get_audit_logs(
|
||||
admin_user_id: Optional[int] = Query(None, description="Filter by admin user"),
|
||||
action: Optional[str] = Query(None, description="Filter by action type"),
|
||||
target_type: Optional[str] = Query(None, description="Filter by target type"),
|
||||
date_from: Optional[datetime] = Query(None, description="Filter from date"),
|
||||
date_to: Optional[datetime] = Query(None, description="Filter to date"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
admin_user_id: Optional[int] = Query(None, description="Filter by admin user"),
|
||||
action: Optional[str] = Query(None, description="Filter by action type"),
|
||||
target_type: Optional[str] = Query(None, description="Filter by target type"),
|
||||
date_from: Optional[datetime] = Query(None, description="Filter from date"),
|
||||
date_to: Optional[datetime] = Query(None, description="Filter to date"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get filtered admin audit logs.
|
||||
@@ -54,7 +52,7 @@ def get_audit_logs(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
logs = admin_audit_service.get_audit_logs(db, filters)
|
||||
@@ -62,19 +60,14 @@ def get_audit_logs(
|
||||
|
||||
logger.info(f"Admin {current_admin.username} retrieved {len(logs)} audit logs")
|
||||
|
||||
return AdminAuditLogListResponse(
|
||||
logs=logs,
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
return AdminAuditLogListResponse(logs=logs, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get("/logs/recent", response_model=list[AdminAuditLogResponse])
|
||||
def get_recent_audit_logs(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get recent audit logs (last 20 by default)."""
|
||||
filters = AdminAuditLogFilters(limit=limit)
|
||||
@@ -83,25 +76,23 @@ def get_recent_audit_logs(
|
||||
|
||||
@router.get("/logs/my-actions", response_model=list[AdminAuditLogResponse])
|
||||
def get_my_actions(
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get audit logs for current admin's actions."""
|
||||
return admin_audit_service.get_recent_actions_by_admin(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
limit=limit
|
||||
db=db, admin_user_id=current_admin.id, limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.get("/logs/target/{target_type}/{target_id}")
|
||||
def get_actions_by_target(
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get all actions performed on a specific target.
|
||||
@@ -109,8 +100,5 @@ def get_actions_by_target(
|
||||
Useful for tracking the history of a specific vendor, user, or entity.
|
||||
"""
|
||||
return admin_audit_service.get_actions_by_target(
|
||||
db=db,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
limit=limit
|
||||
db=db, target_type=target_type, target_id=target_id, limit=limit
|
||||
)
|
||||
|
||||
@@ -10,16 +10,17 @@ This prevents admin cookies from being sent to vendor routes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from app.services.auth_service import auth_service
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from models.schema.auth import LoginResponse, UserLogin, UserResponse
|
||||
from app.services.auth_service import auth_service
|
||||
from models.database.user import User
|
||||
from app.api.deps import get_current_admin_api
|
||||
from models.schema.auth import LoginResponse, UserLogin, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,9 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def admin_login(
|
||||
user_credentials: UserLogin,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
user_credentials: UserLogin, response: Response, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Admin login endpoint.
|
||||
@@ -49,7 +48,9 @@ def admin_login(
|
||||
|
||||
# Verify user is admin
|
||||
if login_result["user"].role != "admin":
|
||||
logger.warning(f"Non-admin user attempted admin login: {user_credentials.email_or_username}")
|
||||
logger.warning(
|
||||
f"Non-admin user attempted admin login: {user_credentials.email_or_username}"
|
||||
)
|
||||
raise InvalidCredentialsException("Admin access required")
|
||||
|
||||
logger.info(f"Admin login successful: {login_result['user'].username}")
|
||||
|
||||
@@ -3,25 +3,27 @@ Code Quality API Endpoints
|
||||
RESTful API for architecture validation and violation management
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.code_quality_service import code_quality_service
|
||||
from app.api.deps import get_current_admin_api
|
||||
from models.database.user import User
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic Models for API
|
||||
|
||||
|
||||
class ScanResponse(BaseModel):
|
||||
"""Response model for a scan"""
|
||||
|
||||
id: int
|
||||
timestamp: str
|
||||
total_files: int
|
||||
@@ -38,6 +40,7 @@ class ScanResponse(BaseModel):
|
||||
|
||||
class ViolationResponse(BaseModel):
|
||||
"""Response model for a violation"""
|
||||
|
||||
id: int
|
||||
scan_id: int
|
||||
rule_id: str
|
||||
@@ -61,6 +64,7 @@ class ViolationResponse(BaseModel):
|
||||
|
||||
class ViolationListResponse(BaseModel):
|
||||
"""Response model for paginated violations list"""
|
||||
|
||||
violations: list[ViolationResponse]
|
||||
total: int
|
||||
page: int
|
||||
@@ -70,34 +74,42 @@ class ViolationListResponse(BaseModel):
|
||||
|
||||
class ViolationDetailResponse(ViolationResponse):
|
||||
"""Response model for single violation with relationships"""
|
||||
|
||||
assignments: list = []
|
||||
comments: list = []
|
||||
|
||||
|
||||
class AssignViolationRequest(BaseModel):
|
||||
"""Request model for assigning a violation"""
|
||||
|
||||
user_id: int = Field(..., description="User ID to assign to")
|
||||
due_date: Optional[datetime] = Field(None, description="Due date for resolution")
|
||||
priority: str = Field("medium", description="Priority level (low, medium, high, critical)")
|
||||
priority: str = Field(
|
||||
"medium", description="Priority level (low, medium, high, critical)"
|
||||
)
|
||||
|
||||
|
||||
class ResolveViolationRequest(BaseModel):
|
||||
"""Request model for resolving a violation"""
|
||||
|
||||
resolution_note: str = Field(..., description="Note about the resolution")
|
||||
|
||||
|
||||
class IgnoreViolationRequest(BaseModel):
|
||||
"""Request model for ignoring a violation"""
|
||||
|
||||
reason: str = Field(..., description="Reason for ignoring")
|
||||
|
||||
|
||||
class AddCommentRequest(BaseModel):
|
||||
"""Request model for adding a comment"""
|
||||
|
||||
comment: str = Field(..., min_length=1, description="Comment text")
|
||||
|
||||
|
||||
class DashboardStatsResponse(BaseModel):
|
||||
"""Response model for dashboard statistics"""
|
||||
|
||||
total_violations: int
|
||||
errors: int
|
||||
warnings: int
|
||||
@@ -116,10 +128,10 @@ class DashboardStatsResponse(BaseModel):
|
||||
|
||||
# API Endpoints
|
||||
|
||||
|
||||
@router.post("/scan", response_model=ScanResponse)
|
||||
async def trigger_scan(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api)
|
||||
):
|
||||
"""
|
||||
Trigger a new architecture scan
|
||||
@@ -127,7 +139,9 @@ async def trigger_scan(
|
||||
Requires authentication. Runs the validator script and stores results.
|
||||
"""
|
||||
try:
|
||||
scan = code_quality_service.run_scan(db, triggered_by=f"manual:{current_user.username}")
|
||||
scan = code_quality_service.run_scan(
|
||||
db, triggered_by=f"manual:{current_user.username}"
|
||||
)
|
||||
|
||||
return ScanResponse(
|
||||
id=scan.id,
|
||||
@@ -138,7 +152,7 @@ async def trigger_scan(
|
||||
warnings=scan.warnings,
|
||||
duration_seconds=scan.duration_seconds,
|
||||
triggered_by=scan.triggered_by,
|
||||
git_commit_hash=scan.git_commit_hash
|
||||
git_commit_hash=scan.git_commit_hash,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}")
|
||||
@@ -148,7 +162,7 @@ async def trigger_scan(
|
||||
async def list_scans(
|
||||
limit: int = Query(30, ge=1, le=100, description="Number of scans to return"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get scan history
|
||||
@@ -167,7 +181,7 @@ async def list_scans(
|
||||
warnings=scan.warnings,
|
||||
duration_seconds=scan.duration_seconds,
|
||||
triggered_by=scan.triggered_by,
|
||||
git_commit_hash=scan.git_commit_hash
|
||||
git_commit_hash=scan.git_commit_hash,
|
||||
)
|
||||
for scan in scans
|
||||
]
|
||||
@@ -175,15 +189,23 @@ async def list_scans(
|
||||
|
||||
@router.get("/violations", response_model=ViolationListResponse)
|
||||
async def list_violations(
|
||||
scan_id: Optional[int] = Query(None, description="Filter by scan ID (defaults to latest)"),
|
||||
severity: Optional[str] = Query(None, description="Filter by severity (error, warning)"),
|
||||
status: Optional[str] = Query(None, description="Filter by status (open, assigned, resolved, ignored)"),
|
||||
scan_id: Optional[int] = Query(
|
||||
None, description="Filter by scan ID (defaults to latest)"
|
||||
),
|
||||
severity: Optional[str] = Query(
|
||||
None, description="Filter by severity (error, warning)"
|
||||
),
|
||||
status: Optional[str] = Query(
|
||||
None, description="Filter by status (open, assigned, resolved, ignored)"
|
||||
),
|
||||
rule_id: Optional[str] = Query(None, description="Filter by rule ID"),
|
||||
file_path: Optional[str] = Query(None, description="Filter by file path (partial match)"),
|
||||
file_path: Optional[str] = Query(
|
||||
None, description="Filter by file path (partial match)"
|
||||
),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(50, ge=1, le=200, description="Items per page"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get violations with filtering and pagination
|
||||
@@ -200,7 +222,7 @@ async def list_violations(
|
||||
rule_id=rule_id,
|
||||
file_path=file_path,
|
||||
limit=page_size,
|
||||
offset=offset
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
@@ -223,14 +245,14 @@ async def list_violations(
|
||||
resolved_at=v.resolved_at.isoformat() if v.resolved_at else None,
|
||||
resolved_by=v.resolved_by,
|
||||
resolution_note=v.resolution_note,
|
||||
created_at=v.created_at.isoformat()
|
||||
created_at=v.created_at.isoformat(),
|
||||
)
|
||||
for v in violations
|
||||
],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=total_pages
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
|
||||
@@ -238,7 +260,7 @@ async def list_violations(
|
||||
async def get_violation(
|
||||
violation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get single violation with details
|
||||
@@ -253,12 +275,12 @@ async def get_violation(
|
||||
# Format assignments
|
||||
assignments = [
|
||||
{
|
||||
'id': a.id,
|
||||
'user_id': a.user_id,
|
||||
'assigned_at': a.assigned_at.isoformat(),
|
||||
'assigned_by': a.assigned_by,
|
||||
'due_date': a.due_date.isoformat() if a.due_date else None,
|
||||
'priority': a.priority
|
||||
"id": a.id,
|
||||
"user_id": a.user_id,
|
||||
"assigned_at": a.assigned_at.isoformat(),
|
||||
"assigned_by": a.assigned_by,
|
||||
"due_date": a.due_date.isoformat() if a.due_date else None,
|
||||
"priority": a.priority,
|
||||
}
|
||||
for a in violation.assignments
|
||||
]
|
||||
@@ -266,10 +288,10 @@ async def get_violation(
|
||||
# Format comments
|
||||
comments = [
|
||||
{
|
||||
'id': c.id,
|
||||
'user_id': c.user_id,
|
||||
'comment': c.comment,
|
||||
'created_at': c.created_at.isoformat()
|
||||
"id": c.id,
|
||||
"user_id": c.user_id,
|
||||
"comment": c.comment,
|
||||
"created_at": c.created_at.isoformat(),
|
||||
}
|
||||
for c in violation.comments
|
||||
]
|
||||
@@ -287,12 +309,14 @@ async def get_violation(
|
||||
suggestion=violation.suggestion,
|
||||
status=violation.status,
|
||||
assigned_to=violation.assigned_to,
|
||||
resolved_at=violation.resolved_at.isoformat() if violation.resolved_at else None,
|
||||
resolved_at=(
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
resolved_by=violation.resolved_by,
|
||||
resolution_note=violation.resolution_note,
|
||||
created_at=violation.created_at.isoformat(),
|
||||
assignments=assignments,
|
||||
comments=comments
|
||||
comments=comments,
|
||||
)
|
||||
|
||||
|
||||
@@ -301,7 +325,7 @@ async def assign_violation(
|
||||
violation_id: int,
|
||||
request: AssignViolationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Assign violation to a developer
|
||||
@@ -315,17 +339,19 @@ async def assign_violation(
|
||||
user_id=request.user_id,
|
||||
assigned_by=current_user.id,
|
||||
due_date=request.due_date,
|
||||
priority=request.priority
|
||||
priority=request.priority,
|
||||
)
|
||||
|
||||
return {
|
||||
'id': assignment.id,
|
||||
'violation_id': assignment.violation_id,
|
||||
'user_id': assignment.user_id,
|
||||
'assigned_at': assignment.assigned_at.isoformat(),
|
||||
'assigned_by': assignment.assigned_by,
|
||||
'due_date': assignment.due_date.isoformat() if assignment.due_date else None,
|
||||
'priority': assignment.priority
|
||||
"id": assignment.id,
|
||||
"violation_id": assignment.violation_id,
|
||||
"user_id": assignment.user_id,
|
||||
"assigned_at": assignment.assigned_at.isoformat(),
|
||||
"assigned_by": assignment.assigned_by,
|
||||
"due_date": (
|
||||
assignment.due_date.isoformat() if assignment.due_date else None
|
||||
),
|
||||
"priority": assignment.priority,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -336,7 +362,7 @@ async def resolve_violation(
|
||||
violation_id: int,
|
||||
request: ResolveViolationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Mark violation as resolved
|
||||
@@ -348,15 +374,17 @@ async def resolve_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
resolved_by=current_user.id,
|
||||
resolution_note=request.resolution_note
|
||||
resolution_note=request.resolution_note,
|
||||
)
|
||||
|
||||
return {
|
||||
'id': violation.id,
|
||||
'status': violation.status,
|
||||
'resolved_at': violation.resolved_at.isoformat() if violation.resolved_at else None,
|
||||
'resolved_by': violation.resolved_by,
|
||||
'resolution_note': violation.resolution_note
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -369,7 +397,7 @@ async def ignore_violation(
|
||||
violation_id: int,
|
||||
request: IgnoreViolationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Mark violation as ignored (won't fix)
|
||||
@@ -381,15 +409,17 @@ async def ignore_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
ignored_by=current_user.id,
|
||||
reason=request.reason
|
||||
reason=request.reason,
|
||||
)
|
||||
|
||||
return {
|
||||
'id': violation.id,
|
||||
'status': violation.status,
|
||||
'resolved_at': violation.resolved_at.isoformat() if violation.resolved_at else None,
|
||||
'resolved_by': violation.resolved_by,
|
||||
'resolution_note': violation.resolution_note
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -402,7 +432,7 @@ async def add_comment(
|
||||
violation_id: int,
|
||||
request: AddCommentRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Add comment to violation
|
||||
@@ -414,15 +444,15 @@ async def add_comment(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
user_id=current_user.id,
|
||||
comment=request.comment
|
||||
comment=request.comment,
|
||||
)
|
||||
|
||||
return {
|
||||
'id': comment.id,
|
||||
'violation_id': comment.violation_id,
|
||||
'user_id': comment.user_id,
|
||||
'comment': comment.comment,
|
||||
'created_at': comment.created_at.isoformat()
|
||||
"id": comment.id,
|
||||
"violation_id": comment.violation_id,
|
||||
"user_id": comment.user_id,
|
||||
"comment": comment.comment,
|
||||
"created_at": comment.created_at.isoformat(),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -430,8 +460,7 @@ async def add_comment(
|
||||
|
||||
@router.get("/stats", response_model=DashboardStatsResponse)
|
||||
async def get_dashboard_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_api)
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api)
|
||||
):
|
||||
"""
|
||||
Get dashboard statistics
|
||||
|
||||
@@ -10,6 +10,7 @@ Platform administrators can:
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -26,24 +27,43 @@ logger = logging.getLogger(__name__)
|
||||
# REQUEST/RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ContentPageCreate(BaseModel):
|
||||
"""Schema for creating a content page."""
|
||||
slug: str = Field(..., max_length=100, description="URL-safe identifier (about, faq, contact, etc.)")
|
||||
|
||||
slug: str = Field(
|
||||
...,
|
||||
max_length=100,
|
||||
description="URL-safe identifier (about, faq, contact, etc.)",
|
||||
)
|
||||
title: str = Field(..., max_length=200, description="Page title")
|
||||
content: str = Field(..., description="HTML or Markdown content")
|
||||
content_format: str = Field(default="html", description="Content format: html or markdown")
|
||||
template: str = Field(default="default", max_length=50, description="Template name (default, minimal, modern)")
|
||||
meta_description: Optional[str] = Field(None, max_length=300, description="SEO meta description")
|
||||
meta_keywords: Optional[str] = Field(None, max_length=300, description="SEO keywords")
|
||||
content_format: str = Field(
|
||||
default="html", description="Content format: html or markdown"
|
||||
)
|
||||
template: str = Field(
|
||||
default="default",
|
||||
max_length=50,
|
||||
description="Template name (default, minimal, modern)",
|
||||
)
|
||||
meta_description: Optional[str] = Field(
|
||||
None, max_length=300, description="SEO meta description"
|
||||
)
|
||||
meta_keywords: Optional[str] = Field(
|
||||
None, max_length=300, description="SEO keywords"
|
||||
)
|
||||
is_published: bool = Field(default=False, description="Publish immediately")
|
||||
show_in_footer: bool = Field(default=True, description="Show in footer navigation")
|
||||
show_in_header: bool = Field(default=False, description="Show in header navigation")
|
||||
display_order: int = Field(default=0, description="Display order (lower = first)")
|
||||
vendor_id: Optional[int] = Field(None, description="Vendor ID (None for platform default)")
|
||||
vendor_id: Optional[int] = Field(
|
||||
None, description="Vendor ID (None for platform default)"
|
||||
)
|
||||
|
||||
|
||||
class ContentPageUpdate(BaseModel):
|
||||
"""Schema for updating a content page."""
|
||||
|
||||
title: Optional[str] = Field(None, max_length=200)
|
||||
content: Optional[str] = None
|
||||
content_format: Optional[str] = None
|
||||
@@ -58,6 +78,7 @@ class ContentPageUpdate(BaseModel):
|
||||
|
||||
class ContentPageResponse(BaseModel):
|
||||
"""Schema for content page response."""
|
||||
|
||||
id: int
|
||||
vendor_id: Optional[int]
|
||||
vendor_name: Optional[str]
|
||||
@@ -84,11 +105,12 @@ class ContentPageResponse(BaseModel):
|
||||
# PLATFORM DEFAULT PAGES (vendor_id=NULL)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/platform", response_model=List[ContentPageResponse])
|
||||
def list_platform_pages(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all platform default content pages.
|
||||
@@ -96,8 +118,7 @@ def list_platform_pages(
|
||||
These are used as fallbacks when vendors haven't created custom pages.
|
||||
"""
|
||||
pages = content_page_service.list_all_platform_pages(
|
||||
db,
|
||||
include_unpublished=include_unpublished
|
||||
db, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
@@ -107,7 +128,7 @@ def list_platform_pages(
|
||||
def create_platform_page(
|
||||
page_data: ContentPageCreate,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a new platform default content page.
|
||||
@@ -129,7 +150,7 @@ def create_platform_page(
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
display_order=page_data.display_order,
|
||||
created_by=current_user.id
|
||||
created_by=current_user.id,
|
||||
)
|
||||
|
||||
return page.to_dict()
|
||||
@@ -139,12 +160,13 @@ def create_platform_page(
|
||||
# ALL CONTENT PAGES (Platform + Vendors)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ContentPageResponse])
|
||||
def list_all_pages(
|
||||
vendor_id: Optional[int] = Query(None, description="Filter by vendor ID"),
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all content pages (platform defaults and vendor overrides).
|
||||
@@ -153,15 +175,14 @@ def list_all_pages(
|
||||
"""
|
||||
if vendor_id:
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=include_unpublished
|
||||
db, vendor_id=vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
else:
|
||||
# Get all pages (both platform and vendor)
|
||||
from models.database.content_page import ContentPage
|
||||
from sqlalchemy import and_
|
||||
|
||||
from models.database.content_page import ContentPage
|
||||
|
||||
filters = []
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
@@ -169,7 +190,9 @@ def list_all_pages(
|
||||
pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters) if filters else True)
|
||||
.order_by(ContentPage.vendor_id, ContentPage.display_order, ContentPage.title)
|
||||
.order_by(
|
||||
ContentPage.vendor_id, ContentPage.display_order, ContentPage.title
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -180,7 +203,7 @@ def list_all_pages(
|
||||
def get_page(
|
||||
page_id: int,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a specific content page by ID."""
|
||||
page = content_page_service.get_page_by_id(db, page_id)
|
||||
@@ -196,7 +219,7 @@ def update_page(
|
||||
page_id: int,
|
||||
page_data: ContentPageUpdate,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update a content page (platform or vendor)."""
|
||||
page = content_page_service.update_page(
|
||||
@@ -212,7 +235,7 @@ def update_page(
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
display_order=page_data.display_order,
|
||||
updated_by=current_user.id
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
|
||||
if not page:
|
||||
@@ -225,7 +248,7 @@ def update_page(
|
||||
def delete_page(
|
||||
page_id: int,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete a content page."""
|
||||
success = content_page_service.delete_page(db, page_id)
|
||||
|
||||
@@ -5,6 +5,7 @@ Admin dashboard and statistics endpoints.
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -87,4 +88,4 @@ def get_platform_statistics(
|
||||
"products": stats_service.get_product_statistics(db),
|
||||
"orders": stats_service.get_order_statistics(db),
|
||||
"imports": stats_service.get_import_statistics(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ from app.api.deps import get_current_admin_api
|
||||
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.schema.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.database.user import User
|
||||
from models.schema.marketplace_import_job import MarketplaceImportJobResponse
|
||||
|
||||
router = APIRouter(prefix="/marketplace-import-jobs")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Platform monitoring and alerts
|
||||
# Platform monitoring and alerts
|
||||
|
||||
@@ -16,16 +16,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from models.schema.admin import (
|
||||
AdminNotificationCreate,
|
||||
AdminNotificationResponse,
|
||||
AdminNotificationListResponse,
|
||||
PlatformAlertCreate,
|
||||
PlatformAlertResponse,
|
||||
PlatformAlertListResponse,
|
||||
PlatformAlertResolve
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.schema.admin import (AdminNotificationCreate,
|
||||
AdminNotificationListResponse,
|
||||
AdminNotificationResponse,
|
||||
PlatformAlertCreate,
|
||||
PlatformAlertListResponse,
|
||||
PlatformAlertResolve, PlatformAlertResponse)
|
||||
|
||||
router = APIRouter(prefix="/notifications")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,6 +32,7 @@ logger = logging.getLogger(__name__)
|
||||
# ADMIN NOTIFICATIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=AdminNotificationListResponse)
|
||||
def get_notifications(
|
||||
priority: Optional[str] = Query(None, description="Filter by priority"),
|
||||
@@ -47,11 +45,7 @@ def get_notifications(
|
||||
"""Get admin notifications with filtering."""
|
||||
# TODO: Implement notification service
|
||||
return AdminNotificationListResponse(
|
||||
notifications=[],
|
||||
total=0,
|
||||
unread_count=0,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
notifications=[], total=0, unread_count=0, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
@@ -90,10 +84,13 @@ def mark_all_as_read(
|
||||
# PLATFORM ALERTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/alerts", response_model=PlatformAlertListResponse)
|
||||
def get_platform_alerts(
|
||||
severity: Optional[str] = Query(None, description="Filter by severity"),
|
||||
is_resolved: Optional[bool] = Query(None, description="Filter by resolution status"),
|
||||
is_resolved: Optional[bool] = Query(
|
||||
None, description="Filter by resolution status"
|
||||
),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -102,12 +99,7 @@ def get_platform_alerts(
|
||||
"""Get platform alerts with filtering."""
|
||||
# TODO: Implement alert service
|
||||
return PlatformAlertListResponse(
|
||||
alerts=[],
|
||||
total=0,
|
||||
active_count=0,
|
||||
critical_count=0,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
alerts=[], total=0, active_count=0, critical_count=0, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
@@ -147,5 +139,5 @@ def get_alert_statistics(
|
||||
"total_alerts": 0,
|
||||
"active_alerts": 0,
|
||||
"critical_alerts": 0,
|
||||
"resolved_today": 0
|
||||
"resolved_today": 0,
|
||||
}
|
||||
|
||||
@@ -16,15 +16,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
from models.schema.admin import (
|
||||
AdminSettingCreate,
|
||||
AdminSettingResponse,
|
||||
AdminSettingUpdate,
|
||||
AdminSettingListResponse
|
||||
)
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from models.database.user import User
|
||||
from models.schema.admin import (AdminSettingCreate, AdminSettingListResponse,
|
||||
AdminSettingResponse, AdminSettingUpdate)
|
||||
|
||||
router = APIRouter(prefix="/settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,10 +28,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("", response_model=AdminSettingListResponse)
|
||||
def get_all_settings(
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
is_public: Optional[bool] = Query(None, description="Filter by public flag"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
is_public: Optional[bool] = Query(None, description="Filter by public flag"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get all platform settings.
|
||||
@@ -46,16 +42,14 @@ def get_all_settings(
|
||||
settings = admin_settings_service.get_all_settings(db, category, is_public)
|
||||
|
||||
return AdminSettingListResponse(
|
||||
settings=settings,
|
||||
total=len(settings),
|
||||
category=category
|
||||
settings=settings, total=len(settings), category=category
|
||||
)
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
def get_setting_categories(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get list of all setting categories."""
|
||||
# This could be enhanced to return counts per category
|
||||
@@ -66,22 +60,23 @@ def get_setting_categories(
|
||||
"marketplace",
|
||||
"notifications",
|
||||
"integrations",
|
||||
"payments"
|
||||
"payments",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=AdminSettingResponse)
|
||||
def get_setting(
|
||||
key: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
key: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get specific setting by key."""
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
|
||||
if not setting:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"Setting '{key}' not found")
|
||||
|
||||
return AdminSettingResponse.model_validate(setting)
|
||||
@@ -89,9 +84,9 @@ def get_setting(
|
||||
|
||||
@router.post("", response_model=AdminSettingResponse)
|
||||
def create_setting(
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Create new platform setting.
|
||||
@@ -99,9 +94,7 @@ def create_setting(
|
||||
Setting keys should be lowercase with underscores (e.g., max_vendors_allowed).
|
||||
"""
|
||||
result = admin_settings_service.create_setting(
|
||||
db=db,
|
||||
setting_data=setting_data,
|
||||
admin_user_id=current_admin.id
|
||||
db=db, setting_data=setting_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
@@ -111,7 +104,10 @@ def create_setting(
|
||||
action="create_setting",
|
||||
target_type="setting",
|
||||
target_id=setting_data.key,
|
||||
details={"category": setting_data.category, "value_type": setting_data.value_type}
|
||||
details={
|
||||
"category": setting_data.category,
|
||||
"value_type": setting_data.value_type,
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -119,19 +115,16 @@ def create_setting(
|
||||
|
||||
@router.put("/{key}", response_model=AdminSettingResponse)
|
||||
def update_setting(
|
||||
key: str,
|
||||
update_data: AdminSettingUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
key: str,
|
||||
update_data: AdminSettingUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Update existing setting value."""
|
||||
old_value = admin_settings_service.get_setting_value(db, key)
|
||||
|
||||
result = admin_settings_service.update_setting(
|
||||
db=db,
|
||||
key=key,
|
||||
update_data=update_data,
|
||||
admin_user_id=current_admin.id
|
||||
db=db, key=key, update_data=update_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
@@ -141,7 +134,7 @@ def update_setting(
|
||||
action="update_setting",
|
||||
target_type="setting",
|
||||
target_id=key,
|
||||
details={"old_value": str(old_value), "new_value": update_data.value}
|
||||
details={"old_value": str(old_value), "new_value": update_data.value},
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -149,9 +142,9 @@ def update_setting(
|
||||
|
||||
@router.post("/upsert", response_model=AdminSettingResponse)
|
||||
def upsert_setting(
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Create or update setting (upsert).
|
||||
@@ -159,9 +152,7 @@ def upsert_setting(
|
||||
If setting exists, updates its value. If not, creates new setting.
|
||||
"""
|
||||
result = admin_settings_service.upsert_setting(
|
||||
db=db,
|
||||
setting_data=setting_data,
|
||||
admin_user_id=current_admin.id
|
||||
db=db, setting_data=setting_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
@@ -171,7 +162,7 @@ def upsert_setting(
|
||||
action="upsert_setting",
|
||||
target_type="setting",
|
||||
target_id=setting_data.key,
|
||||
details={"category": setting_data.category}
|
||||
details={"category": setting_data.category},
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -179,10 +170,10 @@ def upsert_setting(
|
||||
|
||||
@router.delete("/{key}")
|
||||
def delete_setting(
|
||||
key: str,
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
key: str,
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete platform setting.
|
||||
@@ -195,13 +186,11 @@ def delete_setting(
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Deletion requires confirmation parameter: confirm=true"
|
||||
detail="Deletion requires confirmation parameter: confirm=true",
|
||||
)
|
||||
|
||||
message = admin_settings_service.delete_setting(
|
||||
db=db,
|
||||
key=key,
|
||||
admin_user_id=current_admin.id
|
||||
db=db, key=key, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
@@ -211,7 +200,7 @@ def delete_setting(
|
||||
action="delete_setting",
|
||||
target_type="setting",
|
||||
target_id=key,
|
||||
details={}
|
||||
details={},
|
||||
)
|
||||
|
||||
return {"message": message}
|
||||
|
||||
@@ -13,8 +13,8 @@ from app.api.deps import get_current_admin_api
|
||||
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.schema.auth import UserResponse
|
||||
from models.database.user import User
|
||||
from models.schema.auth import UserResponse
|
||||
|
||||
router = APIRouter(prefix="/users")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,24 +12,22 @@ Follows the architecture pattern:
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Body, Query
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.vendor_domain_service import vendor_domain_service
|
||||
from app.exceptions import VendorNotFoundException
|
||||
from models.schema.vendor_domain import (
|
||||
VendorDomainCreate,
|
||||
VendorDomainUpdate,
|
||||
VendorDomainResponse,
|
||||
VendorDomainListResponse,
|
||||
DomainVerificationInstructions,
|
||||
DomainVerificationResponse,
|
||||
DomainDeletionResponse,
|
||||
)
|
||||
from app.services.vendor_domain_service import vendor_domain_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.vendor_domain import (DomainDeletionResponse,
|
||||
DomainVerificationInstructions,
|
||||
DomainVerificationResponse,
|
||||
VendorDomainCreate,
|
||||
VendorDomainListResponse,
|
||||
VendorDomainResponse,
|
||||
VendorDomainUpdate)
|
||||
|
||||
router = APIRouter(prefix="/vendors")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,10 +55,10 @@ def _get_vendor_by_id(db: Session, vendor_id: int) -> Vendor:
|
||||
|
||||
@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
|
||||
def add_vendor_domain(
|
||||
vendor_id: int = Path(..., description="Vendor ID", gt=0),
|
||||
domain_data: VendorDomainCreate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_id: int = Path(..., description="Vendor ID", gt=0),
|
||||
domain_data: VendorDomainCreate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Add a custom domain to vendor (Admin only).
|
||||
@@ -88,9 +86,7 @@ def add_vendor_domain(
|
||||
- 422: Invalid domain format or reserved subdomain
|
||||
"""
|
||||
domain = vendor_domain_service.add_domain(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
domain_data=domain_data
|
||||
db=db, vendor_id=vendor_id, domain_data=domain_data
|
||||
)
|
||||
|
||||
return VendorDomainResponse(
|
||||
@@ -111,9 +107,9 @@ def add_vendor_domain(
|
||||
|
||||
@router.get("/{vendor_id}/domains", response_model=VendorDomainListResponse)
|
||||
def list_vendor_domains(
|
||||
vendor_id: int = Path(..., description="Vendor ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_id: int = Path(..., description="Vendor ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
List all domains for a vendor (Admin only).
|
||||
@@ -148,15 +144,15 @@ def list_vendor_domains(
|
||||
)
|
||||
for d in domains
|
||||
],
|
||||
total=len(domains)
|
||||
total=len(domains),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/domains/{domain_id}", response_model=VendorDomainResponse)
|
||||
def get_domain_details(
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get detailed information about a specific domain (Admin only).
|
||||
@@ -174,7 +170,9 @@ def get_domain_details(
|
||||
is_active=domain.is_active,
|
||||
is_verified=domain.is_verified,
|
||||
ssl_status=domain.ssl_status,
|
||||
verification_token=domain.verification_token if not domain.is_verified else None,
|
||||
verification_token=(
|
||||
domain.verification_token if not domain.is_verified else None
|
||||
),
|
||||
verified_at=domain.verified_at,
|
||||
ssl_verified_at=domain.ssl_verified_at,
|
||||
created_at=domain.created_at,
|
||||
@@ -184,10 +182,10 @@ def get_domain_details(
|
||||
|
||||
@router.put("/domains/{domain_id}", response_model=VendorDomainResponse)
|
||||
def update_vendor_domain(
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
domain_update: VendorDomainUpdate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
domain_update: VendorDomainUpdate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Update domain settings (Admin only).
|
||||
@@ -206,9 +204,7 @@ def update_vendor_domain(
|
||||
- 400: Cannot activate unverified domain
|
||||
"""
|
||||
domain = vendor_domain_service.update_domain(
|
||||
db=db,
|
||||
domain_id=domain_id,
|
||||
domain_update=domain_update
|
||||
db=db, domain_id=domain_id, domain_update=domain_update
|
||||
)
|
||||
|
||||
return VendorDomainResponse(
|
||||
@@ -229,9 +225,9 @@ def update_vendor_domain(
|
||||
|
||||
@router.delete("/domains/{domain_id}", response_model=DomainDeletionResponse)
|
||||
def delete_vendor_domain(
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete a custom domain (Admin only).
|
||||
@@ -250,17 +246,15 @@ def delete_vendor_domain(
|
||||
message = vendor_domain_service.delete_domain(db, domain_id)
|
||||
|
||||
return DomainDeletionResponse(
|
||||
message=message,
|
||||
domain=domain_name,
|
||||
vendor_id=vendor_id
|
||||
message=message, domain=domain_name, vendor_id=vendor_id
|
||||
)
|
||||
|
||||
|
||||
@router.post("/domains/{domain_id}/verify", response_model=DomainVerificationResponse)
|
||||
def verify_domain_ownership(
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Verify domain ownership via DNS TXT record (Admin only).
|
||||
@@ -290,15 +284,18 @@ def verify_domain_ownership(
|
||||
message=message,
|
||||
domain=domain.domain,
|
||||
verified_at=domain.verified_at,
|
||||
is_verified=domain.is_verified
|
||||
is_verified=domain.is_verified,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/domains/{domain_id}/verification-instructions", response_model=DomainVerificationInstructions)
|
||||
@router.get(
|
||||
"/domains/{domain_id}/verification-instructions",
|
||||
response_model=DomainVerificationInstructions,
|
||||
)
|
||||
def get_domain_verification_instructions(
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
domain_id: int = Path(..., description="Domain ID", gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get DNS verification instructions for domain (Admin only).
|
||||
@@ -324,5 +321,5 @@ def get_domain_verification_instructions(
|
||||
verification_token=instructions["verification_token"],
|
||||
instructions=instructions["instructions"],
|
||||
txt_record=instructions["txt_record"],
|
||||
common_registrars=instructions["common_registrars"]
|
||||
common_registrars=instructions["common_registrars"],
|
||||
)
|
||||
|
||||
@@ -20,11 +20,8 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api, get_db
|
||||
from app.services.vendor_theme_service import vendor_theme_service
|
||||
from models.database.user import User
|
||||
from models.schema.vendor_theme import (
|
||||
VendorThemeResponse,
|
||||
VendorThemeUpdate,
|
||||
ThemePresetListResponse
|
||||
)
|
||||
from models.schema.vendor_theme import (ThemePresetListResponse,
|
||||
VendorThemeResponse, VendorThemeUpdate)
|
||||
|
||||
router = APIRouter(prefix="/vendor-themes")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,10 +31,9 @@ logger = logging.getLogger(__name__)
|
||||
# PRESET ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/presets", response_model=ThemePresetListResponse)
|
||||
async def get_theme_presets(
|
||||
current_admin: User = Depends(get_current_admin_api)
|
||||
):
|
||||
async def get_theme_presets(current_admin: User = Depends(get_current_admin_api)):
|
||||
"""
|
||||
Get all available theme presets with preview information.
|
||||
|
||||
@@ -59,11 +55,12 @@ async def get_theme_presets(
|
||||
# THEME RETRIEVAL
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/{vendor_code}", response_model=VendorThemeResponse)
|
||||
async def get_vendor_theme(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get theme configuration for a vendor.
|
||||
@@ -93,12 +90,13 @@ async def get_vendor_theme(
|
||||
# THEME UPDATE
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.put("/{vendor_code}", response_model=VendorThemeResponse)
|
||||
async def update_vendor_theme(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
theme_data: VendorThemeUpdate = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
theme_data: VendorThemeUpdate = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Update or create theme for a vendor.
|
||||
@@ -140,12 +138,13 @@ async def update_vendor_theme(
|
||||
# PRESET APPLICATION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.post("/{vendor_code}/preset/{preset_name}")
|
||||
async def apply_theme_preset(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
preset_name: str = Path(..., description="Preset name"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
preset_name: str = Path(..., description="Preset name"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Apply a theme preset to a vendor.
|
||||
@@ -184,7 +183,7 @@ async def apply_theme_preset(
|
||||
|
||||
return {
|
||||
"message": f"Applied {preset_name} preset successfully",
|
||||
"theme": theme.to_dict()
|
||||
"theme": theme.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
@@ -192,11 +191,12 @@ async def apply_theme_preset(
|
||||
# THEME DELETION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.delete("/{vendor_code}")
|
||||
async def delete_vendor_theme(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete custom theme for a vendor.
|
||||
|
||||
@@ -6,28 +6,24 @@ Vendor management endpoints for admin.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Path, Body
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import (ConfirmationRequiredException,
|
||||
VendorNotFoundException)
|
||||
from app.services.admin_service import admin_service
|
||||
from app.services.stats_service import stats_service
|
||||
from app.exceptions import VendorNotFoundException, ConfirmationRequiredException
|
||||
from models.schema.stats import VendorStatsResponse
|
||||
from models.schema.vendor import (
|
||||
VendorListResponse,
|
||||
VendorResponse,
|
||||
VendorDetailResponse,
|
||||
VendorCreate,
|
||||
VendorCreateResponse,
|
||||
VendorUpdate,
|
||||
VendorTransferOwnership,
|
||||
VendorTransferOwnershipResponse,
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from sqlalchemy import func
|
||||
from models.schema.stats import VendorStatsResponse
|
||||
from models.schema.vendor import (VendorCreate, VendorCreateResponse,
|
||||
VendorDetailResponse, VendorListResponse,
|
||||
VendorResponse, VendorTransferOwnership,
|
||||
VendorTransferOwnershipResponse,
|
||||
VendorUpdate)
|
||||
|
||||
router = APIRouter(prefix="/vendors")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -60,9 +56,11 @@ def _get_vendor_by_identifier(db: Session, identifier: str) -> Vendor:
|
||||
pass
|
||||
|
||||
# Try as vendor_code (case-insensitive)
|
||||
vendor = db.query(Vendor).filter(
|
||||
func.upper(Vendor.vendor_code) == identifier.upper()
|
||||
).first()
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(func.upper(Vendor.vendor_code) == identifier.upper())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(identifier, identifier_type="code")
|
||||
@@ -72,9 +70,9 @@ def _get_vendor_by_identifier(db: Session, identifier: str) -> Vendor:
|
||||
|
||||
@router.post("", response_model=VendorCreateResponse)
|
||||
def create_vendor_with_owner(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Create a new vendor with owner user account (Admin only).
|
||||
@@ -93,8 +91,7 @@ def create_vendor_with_owner(
|
||||
Returns vendor details with owner credentials.
|
||||
"""
|
||||
vendor, owner_user, temp_password = admin_service.create_vendor_with_owner(
|
||||
db=db,
|
||||
vendor_data=vendor_data
|
||||
db=db, vendor_data=vendor_data
|
||||
)
|
||||
|
||||
return VendorCreateResponse(
|
||||
@@ -121,19 +118,19 @@ def create_vendor_with_owner(
|
||||
owner_email=owner_user.email,
|
||||
owner_username=owner_user.username,
|
||||
temporary_password=temp_password,
|
||||
login_url=f"http://localhost:8000/vendor/{vendor.subdomain}/login"
|
||||
login_url=f"http://localhost:8000/vendor/{vendor.subdomain}/login",
|
||||
)
|
||||
|
||||
|
||||
@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_api),
|
||||
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_api),
|
||||
):
|
||||
"""Get all vendors with filtering (Admin only)."""
|
||||
vendors, total = admin_service.get_all_vendors(
|
||||
@@ -142,15 +139,15 @@ def get_all_vendors_admin(
|
||||
limit=limit,
|
||||
search=search,
|
||||
is_active=is_active,
|
||||
is_verified=is_verified
|
||||
is_verified=is_verified,
|
||||
)
|
||||
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=VendorStatsResponse)
|
||||
def get_vendor_statistics_endpoint(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get vendor statistics for admin dashboard (Admin only)."""
|
||||
stats = stats_service.get_vendor_statistics(db)
|
||||
@@ -165,9 +162,9 @@ def get_vendor_statistics_endpoint(
|
||||
|
||||
@router.get("/{vendor_identifier}", response_model=VendorDetailResponse)
|
||||
def get_vendor_details(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get detailed vendor information including owner details (Admin only).
|
||||
@@ -208,10 +205,10 @@ def get_vendor_details(
|
||||
|
||||
@router.put("/{vendor_identifier}", response_model=VendorDetailResponse)
|
||||
def update_vendor(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
vendor_update: VendorUpdate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
vendor_update: VendorUpdate = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Update vendor information (Admin only).
|
||||
@@ -257,12 +254,15 @@ def update_vendor(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{vendor_identifier}/transfer-ownership", response_model=VendorTransferOwnershipResponse)
|
||||
@router.post(
|
||||
"/{vendor_identifier}/transfer-ownership",
|
||||
response_model=VendorTransferOwnershipResponse,
|
||||
)
|
||||
def transfer_vendor_ownership(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
transfer_data: VendorTransferOwnership = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
transfer_data: VendorTransferOwnership = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Transfer vendor ownership to another user (Admin only).
|
||||
@@ -311,10 +311,10 @@ def transfer_vendor_ownership(
|
||||
|
||||
@router.put("/{vendor_identifier}/verification", response_model=VendorDetailResponse)
|
||||
def toggle_vendor_verification(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
verification_data: dict = Body(..., example={"is_verified": True}),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
verification_data: dict = Body(..., example={"is_verified": True}),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Toggle vendor verification status (Admin only).
|
||||
@@ -362,10 +362,10 @@ def toggle_vendor_verification(
|
||||
|
||||
@router.put("/{vendor_identifier}/status", response_model=VendorDetailResponse)
|
||||
def toggle_vendor_status(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
status_data: dict = Body(..., example={"is_active": True}),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
status_data: dict = Body(..., example={"is_active": True}),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Toggle vendor active status (Admin only).
|
||||
@@ -413,10 +413,10 @@ def toggle_vendor_status(
|
||||
|
||||
@router.delete("/{vendor_identifier}")
|
||||
def delete_vendor(
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete vendor and all associated data (Admin only).
|
||||
@@ -436,7 +436,7 @@ def delete_vendor(
|
||||
if not confirm:
|
||||
raise ConfirmationRequiredException(
|
||||
operation="delete_vendor",
|
||||
message="Deletion requires confirmation parameter: confirm=true"
|
||||
message="Deletion requires confirmation parameter: confirm=true",
|
||||
)
|
||||
|
||||
vendor = _get_vendor_by_identifier(db, vendor_identifier)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Health checks
|
||||
# Health checks
|
||||
|
||||
@@ -1 +1 @@
|
||||
# File upload handling
|
||||
# File upload handling
|
||||
|
||||
@@ -21,7 +21,7 @@ Authentication:
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Import shop routers
|
||||
from . import products, cart, orders, auth, content_pages
|
||||
from . import auth, cart, content_pages, orders, products
|
||||
|
||||
# Create shop router
|
||||
router = APIRouter()
|
||||
@@ -43,6 +43,8 @@ router.include_router(cart.router, tags=["shop-cart"])
|
||||
router.include_router(orders.router, tags=["shop-orders"])
|
||||
|
||||
# Content pages (public)
|
||||
router.include_router(content_pages.router, prefix="/content-pages", tags=["shop-content-pages"])
|
||||
router.include_router(
|
||||
content_pages.router, prefix="/content-pages", tags=["shop-content-pages"]
|
||||
)
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
@@ -15,15 +15,16 @@ This prevents:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Response, Request, HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from app.services.customer_service import customer_service
|
||||
from models.schema.auth import UserLogin
|
||||
from models.schema.customer import CustomerRegister, CustomerResponse
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,6 +33,7 @@ logger = logging.getLogger(__name__)
|
||||
# Response model for customer login
|
||||
class CustomerLoginResponse(BaseModel):
|
||||
"""Customer login response with token and customer data."""
|
||||
|
||||
access_token: str
|
||||
token_type: str
|
||||
expires_in: int
|
||||
@@ -40,9 +42,7 @@ class CustomerLoginResponse(BaseModel):
|
||||
|
||||
@router.post("/auth/register", response_model=CustomerResponse)
|
||||
def register_customer(
|
||||
request: Request,
|
||||
customer_data: CustomerRegister,
|
||||
db: Session = Depends(get_db)
|
||||
request: Request, customer_data: CustomerRegister, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Register a new customer for current vendor.
|
||||
@@ -59,12 +59,12 @@ def register_customer(
|
||||
- phone: Customer phone number (optional)
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -73,14 +73,12 @@ def register_customer(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"email": customer_data.email,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Create customer account
|
||||
customer = customer_service.register_customer(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
customer_data=customer_data
|
||||
db=db, vendor_id=vendor.id, customer_data=customer_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -89,7 +87,7 @@ def register_customer(
|
||||
"customer_id": customer.id,
|
||||
"vendor_id": vendor.id,
|
||||
"email": customer.email,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return CustomerResponse.model_validate(customer)
|
||||
@@ -100,7 +98,7 @@ def customer_login(
|
||||
request: Request,
|
||||
user_credentials: UserLogin,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Customer login for current vendor.
|
||||
@@ -121,12 +119,12 @@ def customer_login(
|
||||
- password: Customer password
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -135,33 +133,39 @@ def customer_login(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"email_or_username": user_credentials.email_or_username,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Authenticate customer
|
||||
login_result = customer_service.login_customer(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
credentials=user_credentials
|
||||
db=db, vendor_id=vendor.id, credentials=user_credentials
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Customer login successful: {login_result['customer'].email} for vendor {vendor.subdomain}",
|
||||
extra={
|
||||
"customer_id": login_result['customer'].id,
|
||||
"customer_id": login_result["customer"].id,
|
||||
"vendor_id": vendor.id,
|
||||
"email": login_result['customer'].email,
|
||||
}
|
||||
"email": login_result["customer"].email,
|
||||
},
|
||||
)
|
||||
|
||||
# Calculate cookie path based on vendor access method
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
cookie_path = "/shop" # Default for domain/subdomain access
|
||||
if access_method == "path":
|
||||
# For path-based access like /vendors/wizamart/shop
|
||||
full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/'
|
||||
full_prefix = (
|
||||
vendor_context.get("full_prefix", "/vendor/")
|
||||
if vendor_context
|
||||
else "/vendor/"
|
||||
)
|
||||
cookie_path = f"{full_prefix}{vendor.subdomain}/shop"
|
||||
|
||||
# Set HTTP-only cookie for browser navigation
|
||||
@@ -180,10 +184,10 @@ def customer_login(
|
||||
f"Set customer_token cookie with {login_result['token_data']['expires_in']}s expiry "
|
||||
f"(path={cookie_path}, httponly=True, secure={should_use_secure_cookies()})",
|
||||
extra={
|
||||
"expires_in": login_result['token_data']['expires_in'],
|
||||
"expires_in": login_result["token_data"]["expires_in"],
|
||||
"secure": should_use_secure_cookies(),
|
||||
"cookie_path": cookie_path,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Return full login response
|
||||
@@ -196,10 +200,7 @@ def customer_login(
|
||||
|
||||
|
||||
@router.post("/auth/logout")
|
||||
def customer_logout(
|
||||
request: Request,
|
||||
response: Response
|
||||
):
|
||||
def customer_logout(request: Request, response: Response):
|
||||
"""
|
||||
Customer logout for current vendor.
|
||||
|
||||
@@ -208,24 +209,32 @@ def customer_logout(
|
||||
Client should also remove token from localStorage.
|
||||
"""
|
||||
# Get vendor from middleware (for logging)
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
logger.info(
|
||||
f"Customer logout for vendor {vendor.subdomain if vendor else 'unknown'}",
|
||||
extra={
|
||||
"vendor_id": vendor.id if vendor else None,
|
||||
"vendor_code": vendor.subdomain if vendor else None,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Calculate cookie path based on vendor access method (must match login)
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
cookie_path = "/shop" # Default for domain/subdomain access
|
||||
if access_method == "path" and vendor:
|
||||
# For path-based access like /vendors/wizamart/shop
|
||||
full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/'
|
||||
full_prefix = (
|
||||
vendor_context.get("full_prefix", "/vendor/")
|
||||
if vendor_context
|
||||
else "/vendor/"
|
||||
)
|
||||
cookie_path = f"{full_prefix}{vendor.subdomain}/shop"
|
||||
|
||||
# Clear the cookie (must match path used when setting)
|
||||
@@ -240,11 +249,7 @@ def customer_logout(
|
||||
|
||||
|
||||
@router.post("/auth/forgot-password")
|
||||
def forgot_password(
|
||||
request: Request,
|
||||
email: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def forgot_password(request: Request, email: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Request password reset for customer.
|
||||
|
||||
@@ -255,12 +260,12 @@ def forgot_password(
|
||||
- email: Customer email address
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -269,7 +274,7 @@ def forgot_password(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"email": email,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Implement password reset functionality
|
||||
@@ -278,9 +283,7 @@ def forgot_password(
|
||||
# - Send reset email to customer
|
||||
# - Return success message (don't reveal if email exists)
|
||||
|
||||
logger.info(
|
||||
f"Password reset requested for {email} (vendor: {vendor.subdomain})"
|
||||
)
|
||||
logger.info(f"Password reset requested for {email} (vendor: {vendor.subdomain})")
|
||||
|
||||
return {
|
||||
"message": "If an account exists with this email, a password reset link has been sent."
|
||||
@@ -289,10 +292,7 @@ def forgot_password(
|
||||
|
||||
@router.post("/auth/reset-password")
|
||||
def reset_password(
|
||||
request: Request,
|
||||
reset_token: str,
|
||||
new_password: str,
|
||||
db: Session = Depends(get_db)
|
||||
request: Request, reset_token: str, new_password: str, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Reset customer password using reset token.
|
||||
@@ -304,12 +304,12 @@ def reset_password(
|
||||
- new_password: New password
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -317,7 +317,7 @@ def reset_password(
|
||||
extra={
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Implement password reset
|
||||
@@ -327,9 +327,7 @@ def reset_password(
|
||||
# - Invalidate reset token
|
||||
# - Return success
|
||||
|
||||
logger.info(
|
||||
f"Password reset completed (vendor: {vendor.subdomain})"
|
||||
)
|
||||
logger.info(f"Password reset completed (vendor: {vendor.subdomain})")
|
||||
|
||||
return {
|
||||
"message": "Password reset successfully. You can now log in with your new password."
|
||||
|
||||
@@ -8,18 +8,15 @@ No authentication required - uses session ID for cart tracking.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Path, Body, Request, HTTPException
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.cart_service import cart_service
|
||||
from models.schema.cart import (
|
||||
AddToCartRequest,
|
||||
UpdateCartItemRequest,
|
||||
CartResponse,
|
||||
CartOperationResponse,
|
||||
ClearCartResponse,
|
||||
)
|
||||
from models.schema.cart import (AddToCartRequest, CartOperationResponse,
|
||||
CartResponse, ClearCartResponse,
|
||||
UpdateCartItemRequest)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,6 +26,7 @@ logger = logging.getLogger(__name__)
|
||||
# CART ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/cart/{session_id}", response_model=CartResponse)
|
||||
def get_cart(
|
||||
request: Request,
|
||||
@@ -45,12 +43,12 @@ def get_cart(
|
||||
- session_id: Unique session identifier for the cart
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -59,23 +57,19 @@ def get_cart(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"session_id": session_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
cart = cart_service.get_cart(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
session_id=session_id
|
||||
)
|
||||
cart = cart_service.get_cart(db=db, vendor_id=vendor.id, session_id=session_id)
|
||||
|
||||
logger.info(
|
||||
f"[SHOP_API] get_cart result: {len(cart.get('items', []))} items in cart",
|
||||
extra={
|
||||
"session_id": session_id,
|
||||
"vendor_id": vendor.id,
|
||||
"item_count": len(cart.get('items', [])),
|
||||
"total": cart.get('total', 0),
|
||||
}
|
||||
"item_count": len(cart.get("items", [])),
|
||||
"total": cart.get("total", 0),
|
||||
},
|
||||
)
|
||||
|
||||
return CartResponse.from_service_dict(cart)
|
||||
@@ -102,12 +96,12 @@ def add_to_cart(
|
||||
- quantity: Quantity to add (default: 1)
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -118,7 +112,7 @@ def add_to_cart(
|
||||
"session_id": session_id,
|
||||
"product_id": cart_data.product_id,
|
||||
"quantity": cart_data.quantity,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = cart_service.add_to_cart(
|
||||
@@ -126,7 +120,7 @@ def add_to_cart(
|
||||
vendor_id=vendor.id,
|
||||
session_id=session_id,
|
||||
product_id=cart_data.product_id,
|
||||
quantity=cart_data.quantity
|
||||
quantity=cart_data.quantity,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -134,13 +128,15 @@ def add_to_cart(
|
||||
extra={
|
||||
"session_id": session_id,
|
||||
"result": result,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return CartOperationResponse(**result)
|
||||
|
||||
|
||||
@router.put("/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse)
|
||||
@router.put(
|
||||
"/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse
|
||||
)
|
||||
def update_cart_item(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
@@ -162,12 +158,12 @@ def update_cart_item(
|
||||
- quantity: New quantity (must be >= 1)
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -178,7 +174,7 @@ def update_cart_item(
|
||||
"session_id": session_id,
|
||||
"product_id": product_id,
|
||||
"quantity": cart_data.quantity,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = cart_service.update_cart_item(
|
||||
@@ -186,13 +182,15 @@ def update_cart_item(
|
||||
vendor_id=vendor.id,
|
||||
session_id=session_id,
|
||||
product_id=product_id,
|
||||
quantity=cart_data.quantity
|
||||
quantity=cart_data.quantity,
|
||||
)
|
||||
|
||||
return CartOperationResponse(**result)
|
||||
|
||||
|
||||
@router.delete("/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse)
|
||||
@router.delete(
|
||||
"/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse
|
||||
)
|
||||
def remove_from_cart(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
@@ -210,12 +208,12 @@ def remove_from_cart(
|
||||
- product_id: ID of product to remove
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -225,14 +223,11 @@ def remove_from_cart(
|
||||
"vendor_code": vendor.subdomain,
|
||||
"session_id": session_id,
|
||||
"product_id": product_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = cart_service.remove_from_cart(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
session_id=session_id,
|
||||
product_id=product_id
|
||||
db=db, vendor_id=vendor.id, session_id=session_id, product_id=product_id
|
||||
)
|
||||
|
||||
return CartOperationResponse(**result)
|
||||
@@ -254,12 +249,12 @@ def clear_cart(
|
||||
- session_id: Unique session identifier for the cart
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -268,13 +263,9 @@ def clear_cart(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"session_id": session_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = cart_service.clear_cart(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
session_id=session_id
|
||||
)
|
||||
result = cart_service.clear_cart(db=db, vendor_id=vendor.id, session_id=session_id)
|
||||
|
||||
return ClearCartResponse(**result)
|
||||
|
||||
@@ -8,6 +8,7 @@ No authentication required.
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -23,8 +24,10 @@ logger = logging.getLogger(__name__)
|
||||
# RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class PublicContentPageResponse(BaseModel):
|
||||
"""Public content page response (no internal IDs)."""
|
||||
|
||||
slug: str
|
||||
title: str
|
||||
content: str
|
||||
@@ -36,6 +39,7 @@ class PublicContentPageResponse(BaseModel):
|
||||
|
||||
class ContentPageListItem(BaseModel):
|
||||
"""Content page list item for navigation."""
|
||||
|
||||
slug: str
|
||||
title: str
|
||||
show_in_footer: bool
|
||||
@@ -47,25 +51,21 @@ class ContentPageListItem(BaseModel):
|
||||
# PUBLIC ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/navigation", response_model=List[ContentPageListItem])
|
||||
def get_navigation_pages(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def get_navigation_pages(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Get list of content pages for navigation (footer/header).
|
||||
|
||||
Uses vendor from request.state (set by middleware).
|
||||
Returns vendor overrides + platform defaults.
|
||||
"""
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_id = vendor.id if vendor else None
|
||||
|
||||
# Get all published pages for this vendor
|
||||
pages = content_page_service.list_pages_for_vendor(
|
||||
db,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=False
|
||||
db, vendor_id=vendor_id, include_unpublished=False
|
||||
)
|
||||
|
||||
return [
|
||||
@@ -81,25 +81,21 @@ def get_navigation_pages(
|
||||
|
||||
|
||||
@router.get("/{slug}", response_model=PublicContentPageResponse)
|
||||
def get_content_page(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def get_content_page(slug: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Get a specific content page by slug.
|
||||
|
||||
Uses vendor from request.state (set by middleware).
|
||||
Returns vendor override if exists, otherwise platform default.
|
||||
"""
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_id = vendor.id if vendor else None
|
||||
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=False # Only show published pages
|
||||
include_unpublished=False, # Only show published pages
|
||||
)
|
||||
|
||||
if not page:
|
||||
|
||||
@@ -10,31 +10,23 @@ Requires customer authentication for most operations.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_customer_api
|
||||
from app.services.order_service import order_service
|
||||
from app.core.database import get_db
|
||||
from app.services.customer_service import customer_service
|
||||
from models.schema.order import (
|
||||
OrderCreate,
|
||||
OrderResponse,
|
||||
OrderDetailResponse,
|
||||
OrderListResponse
|
||||
)
|
||||
from models.database.user import User
|
||||
from app.services.order_service import order_service
|
||||
from models.database.customer import Customer
|
||||
from models.database.user import User
|
||||
from models.schema.order import (OrderCreate, OrderDetailResponse,
|
||||
OrderListResponse, OrderResponse)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_customer_from_user(
|
||||
request: Request,
|
||||
user: User,
|
||||
db: Session
|
||||
) -> Customer:
|
||||
def get_customer_from_user(request: Request, user: User, db: Session) -> Customer:
|
||||
"""
|
||||
Helper to get Customer record from authenticated User.
|
||||
|
||||
@@ -49,25 +41,22 @@ def get_customer_from_user(
|
||||
Raises:
|
||||
HTTPException: If customer not found or vendor mismatch
|
||||
"""
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
# Find customer record for this user and vendor
|
||||
customer = customer_service.get_customer_by_user_id(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
user_id=user.id
|
||||
db=db, vendor_id=vendor.id, user_id=user.id
|
||||
)
|
||||
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Customer account not found for current vendor"
|
||||
status_code=404, detail="Customer account not found for current vendor"
|
||||
)
|
||||
|
||||
return customer
|
||||
@@ -91,12 +80,12 @@ def place_order(
|
||||
- Order data including shipping address, payment method, etc.
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
# Get customer record
|
||||
@@ -109,14 +98,12 @@ def place_order(
|
||||
"vendor_code": vendor.subdomain,
|
||||
"customer_id": customer.id,
|
||||
"user_id": current_user.id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Create order
|
||||
order = order_service.create_order(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_data=order_data
|
||||
db=db, vendor_id=vendor.id, order_data=order_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -127,7 +114,7 @@ def place_order(
|
||||
"order_number": order.order_number,
|
||||
"customer_id": customer.id,
|
||||
"total_amount": float(order.total_amount),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Update customer stats
|
||||
@@ -156,12 +143,12 @@ def get_my_orders(
|
||||
- limit: Maximum number of orders to return
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
# Get customer record
|
||||
@@ -175,23 +162,19 @@ def get_my_orders(
|
||||
"customer_id": customer.id,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Get orders
|
||||
orders, total = order_service.get_customer_orders(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
customer_id=customer.id,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
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
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,12 +195,12 @@ def get_order_details(
|
||||
- order_id: ID of the order to retrieve
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
# Get customer record
|
||||
@@ -230,19 +213,16 @@ def get_order_details(
|
||||
"vendor_code": vendor.subdomain,
|
||||
"customer_id": customer.id,
|
||||
"order_id": order_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Get order
|
||||
order = order_service.get_order(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_id=order_id
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -10,12 +10,13 @@ No authentication required.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Path, Request, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.product_service import product_service
|
||||
from models.schema.product import ProductResponse, ProductDetailResponse, ProductListResponse
|
||||
from models.schema.product import (ProductDetailResponse, ProductListResponse,
|
||||
ProductResponse)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,7 +28,9 @@ def get_product_catalog(
|
||||
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, description="Filter by featured products"),
|
||||
is_featured: Optional[bool] = Query(
|
||||
None, description="Filter by featured products"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -44,12 +47,12 @@ def get_product_catalog(
|
||||
- is_featured: Filter by featured products only
|
||||
"""
|
||||
# Get vendor from middleware (injected by VendorContextMiddleware)
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -61,7 +64,7 @@ def get_product_catalog(
|
||||
"limit": limit,
|
||||
"search": search,
|
||||
"is_featured": is_featured,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Get only active products for public view
|
||||
@@ -71,14 +74,14 @@ def get_product_catalog(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=True, # Only show active products to customers
|
||||
is_featured=is_featured
|
||||
is_featured=is_featured,
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=[ProductResponse.model_validate(p) for p in products],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -98,12 +101,12 @@ def get_product_details(
|
||||
- product_id: ID of the product to retrieve
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -112,18 +115,17 @@ def get_product_details(
|
||||
"vendor_id": vendor.id,
|
||||
"vendor_code": vendor.subdomain,
|
||||
"product_id": product_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
product = product_service.get_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_id=product_id
|
||||
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)
|
||||
@@ -150,12 +152,12 @@ def search_products(
|
||||
- limit: Maximum number of results to return
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path."
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -166,22 +168,18 @@ def search_products(
|
||||
"query": q,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Implement full-text 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
|
||||
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
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
28
app/api/v1/vendor/__init__.py
vendored
28
app/api/v1/vendor/__init__.py
vendored
@@ -13,25 +13,9 @@ IMPORTANT:
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Import all sub-routers (JSON API only)
|
||||
from . import (
|
||||
info,
|
||||
auth,
|
||||
dashboard,
|
||||
profile,
|
||||
settings,
|
||||
products,
|
||||
orders,
|
||||
customers,
|
||||
team,
|
||||
inventory,
|
||||
marketplace,
|
||||
payments,
|
||||
media,
|
||||
notifications,
|
||||
analytics,
|
||||
content_pages,
|
||||
)
|
||||
|
||||
from . import (analytics, auth, content_pages, customers, dashboard, info,
|
||||
inventory, marketplace, media, notifications, orders, payments,
|
||||
products, profile, settings, team)
|
||||
|
||||
# Create vendor router
|
||||
router = APIRouter()
|
||||
@@ -68,7 +52,11 @@ router.include_router(notifications.router, tags=["vendor-notifications"])
|
||||
router.include_router(analytics.router, tags=["vendor-analytics"])
|
||||
|
||||
# Content pages management
|
||||
router.include_router(content_pages.router, prefix="/{vendor_code}/content-pages", tags=["vendor-content-pages"])
|
||||
router.include_router(
|
||||
content_pages.router,
|
||||
prefix="/{vendor_code}/content-pages",
|
||||
tags=["vendor-content-pages"],
|
||||
)
|
||||
|
||||
# Vendor info endpoint - MUST BE LAST! Has catch-all GET /{vendor_code}
|
||||
router.include_router(info.router, tags=["vendor-info"])
|
||||
|
||||
11
app/api/v1/vendor/analytics.py
vendored
11
app/api/v1/vendor/analytics.py
vendored
@@ -4,13 +4,14 @@ Vendor analytics and reporting endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
@@ -20,10 +21,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data for specified time period."""
|
||||
return stats_service.get_vendor_analytics(db, vendor.id, period)
|
||||
|
||||
77
app/api/v1/vendor/auth.py
vendored
77
app/api/v1/vendor/auth.py
vendored
@@ -13,19 +13,20 @@ This prevents:
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
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.schema.auth import UserLogin
|
||||
from models.database.vendor import Vendor, VendorUser, Role
|
||||
from models.database.user import User
|
||||
from pydantic import BaseModel
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from app.services.auth_service import auth_service
|
||||
from middleware.vendor_context import get_current_vendor
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Role, Vendor, VendorUser
|
||||
from models.schema.auth import UserLogin
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,10 +44,10 @@ class VendorLoginResponse(BaseModel):
|
||||
|
||||
@router.post("/login", response_model=VendorLoginResponse)
|
||||
def vendor_login(
|
||||
user_credentials: UserLogin,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
user_credentials: UserLogin,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Vendor team member login.
|
||||
@@ -64,13 +65,16 @@ def vendor_login(
|
||||
vendor = get_current_vendor(request)
|
||||
|
||||
# If no vendor from middleware, try to get from request body
|
||||
if not vendor and hasattr(user_credentials, 'vendor_code'):
|
||||
vendor_code = getattr(user_credentials, 'vendor_code', None)
|
||||
if not vendor and hasattr(user_credentials, "vendor_code"):
|
||||
vendor_code = getattr(user_credentials, "vendor_code", None)
|
||||
if vendor_code:
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.vendor_code == vendor_code.upper(),
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(
|
||||
Vendor.vendor_code == vendor_code.upper(), Vendor.is_active == True
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Authenticate user
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
@@ -79,7 +83,9 @@ def vendor_login(
|
||||
# CRITICAL: Prevent admin users from using vendor login
|
||||
if user.role == "admin":
|
||||
logger.warning(f"Admin user attempted vendor login: {user.username}")
|
||||
raise InvalidCredentialsException("Admins cannot access vendor portal. Please use admin portal.")
|
||||
raise InvalidCredentialsException(
|
||||
"Admins cannot access vendor portal. Please use admin portal."
|
||||
)
|
||||
|
||||
# Determine vendor and role
|
||||
vendor_role = "Member"
|
||||
@@ -92,11 +98,16 @@ def vendor_login(
|
||||
vendor_role = "Owner"
|
||||
else:
|
||||
# Check if user is team member
|
||||
vendor_user = db.query(VendorUser).join(Role).filter(
|
||||
VendorUser.user_id == user.id,
|
||||
VendorUser.vendor_id == vendor.id,
|
||||
VendorUser.is_active == True
|
||||
).first()
|
||||
vendor_user = (
|
||||
db.query(VendorUser)
|
||||
.join(Role)
|
||||
.filter(
|
||||
VendorUser.user_id == user.id,
|
||||
VendorUser.vendor_id == vendor.id,
|
||||
VendorUser.is_active == True,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if vendor_user:
|
||||
vendor_role = vendor_user.role.name
|
||||
@@ -117,17 +128,14 @@ def vendor_login(
|
||||
# Check vendor memberships
|
||||
elif user.vendor_memberships:
|
||||
active_membership = next(
|
||||
(vm for vm in user.vendor_memberships if vm.is_active),
|
||||
None
|
||||
(vm for vm in user.vendor_memberships if vm.is_active), None
|
||||
)
|
||||
if active_membership:
|
||||
vendor = active_membership.vendor
|
||||
vendor_role = active_membership.role.name
|
||||
|
||||
if not vendor:
|
||||
raise InvalidCredentialsException(
|
||||
"User is not associated with any vendor"
|
||||
)
|
||||
raise InvalidCredentialsException("User is not associated with any vendor")
|
||||
|
||||
logger.info(
|
||||
f"Vendor team login successful: {user.username} "
|
||||
@@ -161,7 +169,7 @@ def vendor_login(
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active
|
||||
"is_active": user.is_active,
|
||||
},
|
||||
vendor={
|
||||
"id": vendor.id,
|
||||
@@ -169,9 +177,9 @@ def vendor_login(
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified
|
||||
"is_verified": vendor.is_verified,
|
||||
},
|
||||
vendor_role=vendor_role
|
||||
vendor_role=vendor_role,
|
||||
)
|
||||
|
||||
|
||||
@@ -198,8 +206,7 @@ def vendor_logout(response: Response):
|
||||
|
||||
@router.get("/me")
|
||||
def get_current_vendor_user(
|
||||
user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get current authenticated vendor user.
|
||||
@@ -212,5 +219,5 @@ def get_current_vendor_user(
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
82
app/api/v1/vendor/content_pages.py
vendored
82
app/api/v1/vendor/content_pages.py
vendored
@@ -10,6 +10,7 @@ Vendors can:
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -26,14 +27,26 @@ logger = logging.getLogger(__name__)
|
||||
# REQUEST/RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class VendorContentPageCreate(BaseModel):
|
||||
"""Schema for creating a vendor content page."""
|
||||
slug: str = Field(..., max_length=100, description="URL-safe identifier (about, faq, contact, etc.)")
|
||||
|
||||
slug: str = Field(
|
||||
...,
|
||||
max_length=100,
|
||||
description="URL-safe identifier (about, faq, contact, etc.)",
|
||||
)
|
||||
title: str = Field(..., max_length=200, description="Page title")
|
||||
content: str = Field(..., description="HTML or Markdown content")
|
||||
content_format: str = Field(default="html", description="Content format: html or markdown")
|
||||
meta_description: Optional[str] = Field(None, max_length=300, description="SEO meta description")
|
||||
meta_keywords: Optional[str] = Field(None, max_length=300, description="SEO keywords")
|
||||
content_format: str = Field(
|
||||
default="html", description="Content format: html or markdown"
|
||||
)
|
||||
meta_description: Optional[str] = Field(
|
||||
None, max_length=300, description="SEO meta description"
|
||||
)
|
||||
meta_keywords: Optional[str] = Field(
|
||||
None, max_length=300, description="SEO keywords"
|
||||
)
|
||||
is_published: bool = Field(default=False, description="Publish immediately")
|
||||
show_in_footer: bool = Field(default=True, description="Show in footer navigation")
|
||||
show_in_header: bool = Field(default=False, description="Show in header navigation")
|
||||
@@ -42,6 +55,7 @@ class VendorContentPageCreate(BaseModel):
|
||||
|
||||
class VendorContentPageUpdate(BaseModel):
|
||||
"""Schema for updating a vendor content page."""
|
||||
|
||||
title: Optional[str] = Field(None, max_length=200)
|
||||
content: Optional[str] = None
|
||||
content_format: Optional[str] = None
|
||||
@@ -55,6 +69,7 @@ class VendorContentPageUpdate(BaseModel):
|
||||
|
||||
class ContentPageResponse(BaseModel):
|
||||
"""Schema for content page response."""
|
||||
|
||||
id: int
|
||||
vendor_id: Optional[int]
|
||||
vendor_name: Optional[str]
|
||||
@@ -81,11 +96,12 @@ class ContentPageResponse(BaseModel):
|
||||
# VENDOR CONTENT PAGES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ContentPageResponse])
|
||||
def list_vendor_pages(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all content pages available for this vendor.
|
||||
@@ -93,12 +109,12 @@ def list_vendor_pages(
|
||||
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
pages = content_page_service.list_pages_for_vendor(
|
||||
db,
|
||||
vendor_id=current_user.vendor_id,
|
||||
include_unpublished=include_unpublished
|
||||
db, vendor_id=current_user.vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
@@ -108,7 +124,7 @@ def list_vendor_pages(
|
||||
def list_vendor_overrides(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List only vendor-specific content pages (no platform defaults).
|
||||
@@ -116,12 +132,12 @@ def list_vendor_overrides(
|
||||
Shows what the vendor has customized.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db,
|
||||
vendor_id=current_user.vendor_id,
|
||||
include_unpublished=include_unpublished
|
||||
db, vendor_id=current_user.vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
@@ -132,7 +148,7 @@ def get_page(
|
||||
slug: str,
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific content page by slug.
|
||||
@@ -140,13 +156,15 @@ def get_page(
|
||||
Returns vendor override if exists, otherwise platform default.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=current_user.vendor_id,
|
||||
include_unpublished=include_unpublished
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
|
||||
if not page:
|
||||
@@ -159,7 +177,7 @@ def get_page(
|
||||
def create_vendor_page(
|
||||
page_data: VendorContentPageCreate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a vendor-specific content page override.
|
||||
@@ -167,7 +185,9 @@ def create_vendor_page(
|
||||
This will be shown instead of the platform default for this vendor.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
page = content_page_service.create_page(
|
||||
db,
|
||||
@@ -182,7 +202,7 @@ def create_vendor_page(
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
display_order=page_data.display_order,
|
||||
created_by=current_user.id
|
||||
created_by=current_user.id,
|
||||
)
|
||||
|
||||
return page.to_dict()
|
||||
@@ -193,7 +213,7 @@ def update_vendor_page(
|
||||
page_id: int,
|
||||
page_data: VendorContentPageUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a vendor-specific content page.
|
||||
@@ -201,7 +221,9 @@ def update_vendor_page(
|
||||
Can only update pages owned by this vendor.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
# Verify ownership
|
||||
existing_page = content_page_service.get_page_by_id(db, page_id)
|
||||
@@ -209,7 +231,9 @@ def update_vendor_page(
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
if existing_page.vendor_id != current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="Cannot edit pages from other vendors")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Cannot edit pages from other vendors"
|
||||
)
|
||||
|
||||
# Update
|
||||
page = content_page_service.update_page(
|
||||
@@ -224,7 +248,7 @@ def update_vendor_page(
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
display_order=page_data.display_order,
|
||||
updated_by=current_user.id
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
|
||||
return page.to_dict()
|
||||
@@ -234,7 +258,7 @@ def update_vendor_page(
|
||||
def delete_vendor_page(
|
||||
page_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete a vendor-specific content page.
|
||||
@@ -243,7 +267,9 @@ def delete_vendor_page(
|
||||
After deletion, platform default will be shown (if exists).
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="User is not associated with a vendor")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
|
||||
# Verify ownership
|
||||
existing_page = content_page_service.get_page_by_id(db, page_id)
|
||||
@@ -251,7 +277,9 @@ def delete_vendor_page(
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
if existing_page.vendor_id != current_user.vendor_id:
|
||||
raise HTTPException(status_code=403, detail="Cannot delete pages from other vendors")
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Cannot delete pages from other vendors"
|
||||
)
|
||||
|
||||
# Delete
|
||||
content_page_service.delete_page(db, page_id)
|
||||
|
||||
79
app/api/v1/vendor/customers.py
vendored
79
app/api/v1/vendor/customers.py
vendored
@@ -6,6 +6,7 @@ Vendor customer management endpoints.
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -21,13 +22,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_customers(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all customers for this vendor.
|
||||
@@ -43,16 +44,16 @@ def get_vendor_customers(
|
||||
"total": 0,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"message": "Customer management coming in Slice 4"
|
||||
"message": "Customer management coming in Slice 4",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}")
|
||||
def get_customer_details(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed customer information.
|
||||
@@ -63,17 +64,15 @@ def get_customer_details(
|
||||
- Include order history
|
||||
- Include total spent, etc.
|
||||
"""
|
||||
return {
|
||||
"message": "Customer details coming in Slice 4"
|
||||
}
|
||||
return {"message": "Customer details coming in Slice 4"}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/orders")
|
||||
def get_customer_orders(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get order history for a specific customer.
|
||||
@@ -83,19 +82,16 @@ def get_customer_orders(
|
||||
- Filter by vendor_id
|
||||
- Return order details
|
||||
"""
|
||||
return {
|
||||
"orders": [],
|
||||
"message": "Customer orders coming in Slice 5"
|
||||
}
|
||||
return {"orders": [], "message": "Customer orders coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/{customer_id}")
|
||||
def update_customer(
|
||||
customer_id: int,
|
||||
customer_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
customer_id: int,
|
||||
customer_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update customer information.
|
||||
@@ -105,17 +101,15 @@ def update_customer(
|
||||
- Verify customer belongs to vendor
|
||||
- Update customer preferences
|
||||
"""
|
||||
return {
|
||||
"message": "Customer update coming in Slice 4"
|
||||
}
|
||||
return {"message": "Customer update coming in Slice 4"}
|
||||
|
||||
|
||||
@router.put("/{customer_id}/status")
|
||||
def toggle_customer_status(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Activate/deactivate customer account.
|
||||
@@ -125,17 +119,15 @@ def toggle_customer_status(
|
||||
- Verify customer belongs to vendor
|
||||
- Log the change
|
||||
"""
|
||||
return {
|
||||
"message": "Customer status toggle coming in Slice 4"
|
||||
}
|
||||
return {"message": "Customer status toggle coming in Slice 4"}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/stats")
|
||||
def get_customer_statistics(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get customer statistics and metrics.
|
||||
@@ -151,6 +143,5 @@ def get_customer_statistics(
|
||||
"total_spent": 0.0,
|
||||
"average_order_value": 0.0,
|
||||
"last_order_date": None,
|
||||
"message": "Customer statistics coming in Slice 4"
|
||||
"message": "Customer statistics coming in Slice 4",
|
||||
}
|
||||
|
||||
|
||||
28
app/api/v1/vendor/dashboard.py
vendored
28
app/api/v1/vendor/dashboard.py
vendored
@@ -4,13 +4,14 @@ Vendor dashboard and statistics endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
@@ -20,9 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/stats")
|
||||
def get_vendor_dashboard_stats(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor-specific dashboard statistics.
|
||||
@@ -38,24 +39,23 @@ def get_vendor_dashboard_stats(
|
||||
"""
|
||||
# Get vendor from authenticated user's vendor_user record
|
||||
from models.database.vendor import VendorUser
|
||||
vendor_user = db.query(VendorUser).filter(
|
||||
VendorUser.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
vendor_user = (
|
||||
db.query(VendorUser).filter(VendorUser.user_id == current_user.id).first()
|
||||
)
|
||||
|
||||
if not vendor_user:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="User is not associated with any vendor"
|
||||
status_code=403, detail="User is not associated with any vendor"
|
||||
)
|
||||
|
||||
vendor = vendor_user.vendor
|
||||
if not vendor or not vendor.is_active:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found or inactive"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail="Vendor not found or inactive")
|
||||
|
||||
# Get vendor-scoped statistics
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor.id)
|
||||
@@ -82,5 +82,5 @@ def get_vendor_dashboard_stats(
|
||||
"revenue": {
|
||||
"total": stats_data.get("total_revenue", 0),
|
||||
"this_month": stats_data.get("revenue_this_month", 0),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
23
app/api/v1/vendor/info.py
vendored
23
app/api/v1/vendor/info.py
vendored
@@ -8,14 +8,15 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Path, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import VendorNotFoundException
|
||||
from models.schema.vendor import VendorResponse, VendorDetailResponse
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.vendor import VendorDetailResponse, VendorResponse
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,10 +36,14 @@ def _get_vendor_by_code(db: Session, vendor_code: str) -> Vendor:
|
||||
Raises:
|
||||
VendorNotFoundException: If vendor not found or inactive
|
||||
"""
|
||||
vendor = db.query(Vendor).filter(
|
||||
func.upper(Vendor.vendor_code) == vendor_code.upper(),
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(
|
||||
func.upper(Vendor.vendor_code) == vendor_code.upper(),
|
||||
Vendor.is_active == True,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not vendor:
|
||||
logger.warning(f"Vendor not found or inactive: {vendor_code}")
|
||||
@@ -49,8 +54,8 @@ def _get_vendor_by_code(db: Session, vendor_code: str) -> Vendor:
|
||||
|
||||
@router.get("/{vendor_code}", response_model=VendorDetailResponse)
|
||||
def get_vendor_info(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get public vendor information by vendor code.
|
||||
|
||||
104
app/api/v1/vendor/inventory.py
vendored
104
app/api/v1/vendor/inventory.py
vendored
@@ -7,19 +7,14 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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.schema.inventory import (
|
||||
InventoryCreate,
|
||||
InventoryAdjust,
|
||||
InventoryUpdate,
|
||||
InventoryReserve,
|
||||
InventoryResponse,
|
||||
ProductInventorySummary,
|
||||
InventoryListResponse
|
||||
)
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.inventory import (InventoryAdjust, InventoryCreate,
|
||||
InventoryListResponse, InventoryReserve,
|
||||
InventoryResponse, InventoryUpdate,
|
||||
ProductInventorySummary)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,10 +22,10 @@ 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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
inventory: InventoryCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
return inventory_service.set_inventory(db, vendor.id, inventory)
|
||||
@@ -38,10 +33,10 @@ def set_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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
adjustment: InventoryAdjust,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Adjust inventory (positive to add, negative to remove)."""
|
||||
return inventory_service.adjust_inventory(db, vendor.id, adjustment)
|
||||
@@ -49,10 +44,10 @@ def adjust_inventory(
|
||||
|
||||
@router.post("/inventory/reserve", response_model=InventoryResponse)
|
||||
def reserve_inventory(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Reserve inventory for an order."""
|
||||
return inventory_service.reserve_inventory(db, vendor.id, reservation)
|
||||
@@ -60,10 +55,10 @@ def reserve_inventory(
|
||||
|
||||
@router.post("/inventory/release", response_model=InventoryResponse)
|
||||
def release_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Release reserved inventory (cancel order)."""
|
||||
return inventory_service.release_reservation(db, vendor.id, reservation)
|
||||
@@ -71,10 +66,10 @@ def release_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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Fulfill reservation (complete order, remove from stock)."""
|
||||
return inventory_service.fulfill_reservation(db, vendor.id, reservation)
|
||||
@@ -82,10 +77,10 @@ def fulfill_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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get inventory summary for a product."""
|
||||
return inventory_service.get_product_inventory(db, vendor.id, product_id)
|
||||
@@ -93,13 +88,13 @@ def get_product_inventory(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all inventory for vendor."""
|
||||
inventories = inventory_service.get_vendor_inventory(
|
||||
@@ -110,31 +105,30 @@ def get_vendor_inventory(
|
||||
total = len(inventories) # You might want a separate count query for large datasets
|
||||
|
||||
return InventoryListResponse(
|
||||
inventories=inventories,
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update inventory entry."""
|
||||
return inventory_service.update_inventory(db, vendor.id, inventory_id, inventory_update)
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
inventory_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
inventory_service.delete_inventory(db, vendor.id, inventory_id)
|
||||
|
||||
48
app/api/v1/vendor/marketplace.py
vendored
48
app/api/v1/vendor/marketplace.py
vendored
@@ -12,16 +12,15 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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.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.schema.marketplace_import_job import (
|
||||
MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobRequest
|
||||
)
|
||||
from middleware.vendor_context import require_vendor_context # IMPORTANT
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.marketplace_import_job import (MarketplaceImportJobRequest,
|
||||
MarketplaceImportJobResponse)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,11 +29,11 @@ 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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
request: MarketplaceImportJobRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ADDED: Vendor from middleware
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||
logger.info(
|
||||
@@ -66,7 +65,7 @@ async def import_products_from_marketplace(
|
||||
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}",
|
||||
f"Check status with /import-status/{import_job.id}",
|
||||
imported=0,
|
||||
updated=0,
|
||||
total_processed=0,
|
||||
@@ -77,10 +76,10 @@ async def import_products_from_marketplace(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
job_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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)
|
||||
@@ -88,6 +87,7 @@ def get_marketplace_import_status(
|
||||
# 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)
|
||||
@@ -95,12 +95,12 @@ def get_marketplace_import_status(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get marketplace import jobs for current vendor (Protected)."""
|
||||
jobs = marketplace_import_job_service.get_import_jobs(
|
||||
@@ -112,4 +112,6 @@ def get_marketplace_import_jobs(
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return [marketplace_import_job_service.convert_to_response_model(job) for job in jobs]
|
||||
return [
|
||||
marketplace_import_job_service.convert_to_response_model(job) for job in jobs
|
||||
]
|
||||
|
||||
104
app/api/v1/vendor/media.py
vendored
104
app/api/v1/vendor/media.py
vendored
@@ -6,7 +6,8 @@ Vendor media and file management endpoints.
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query, UploadFile, File
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Query, UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
@@ -21,13 +22,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
def get_media_library(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
media_type: Optional[str] = Query(None, description="image, video, document"),
|
||||
search: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
media_type: Optional[str] = Query(None, description="image, video, document"),
|
||||
search: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor media library.
|
||||
@@ -44,17 +45,17 @@ def get_media_library(
|
||||
"total": 0,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"message": "Media library coming in Slice 3"
|
||||
"message": "Media library coming in Slice 3",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_media(
|
||||
file: UploadFile = File(...),
|
||||
folder: Optional[str] = Query(None, description="products, general, etc."),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
file: UploadFile = File(...),
|
||||
folder: Optional[str] = Query(None, description="products, general, etc."),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload media file.
|
||||
@@ -70,17 +71,17 @@ async def upload_media(
|
||||
return {
|
||||
"file_url": None,
|
||||
"thumbnail_url": None,
|
||||
"message": "Media upload coming in Slice 3"
|
||||
"message": "Media upload coming in Slice 3",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/upload/multiple")
|
||||
async def upload_multiple_media(
|
||||
files: list[UploadFile] = File(...),
|
||||
folder: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
files: list[UploadFile] = File(...),
|
||||
folder: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload multiple media files at once.
|
||||
@@ -94,16 +95,16 @@ async def upload_multiple_media(
|
||||
return {
|
||||
"uploaded_files": [],
|
||||
"failed_files": [],
|
||||
"message": "Multiple upload coming in Slice 3"
|
||||
"message": "Multiple upload coming in Slice 3",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{media_id}")
|
||||
def get_media_details(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get media file details.
|
||||
@@ -113,18 +114,16 @@ def get_media_details(
|
||||
- Return file URL
|
||||
- Return usage information (which products use this file)
|
||||
"""
|
||||
return {
|
||||
"message": "Media details coming in Slice 3"
|
||||
}
|
||||
return {"message": "Media details coming in Slice 3"}
|
||||
|
||||
|
||||
@router.put("/{media_id}")
|
||||
def update_media_metadata(
|
||||
media_id: int,
|
||||
metadata: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
media_id: int,
|
||||
metadata: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update media file metadata.
|
||||
@@ -135,17 +134,15 @@ def update_media_metadata(
|
||||
- Update tags/categories
|
||||
- Update description
|
||||
"""
|
||||
return {
|
||||
"message": "Media update coming in Slice 3"
|
||||
}
|
||||
return {"message": "Media update coming in Slice 3"}
|
||||
|
||||
|
||||
@router.delete("/{media_id}")
|
||||
def delete_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete media file.
|
||||
@@ -157,17 +154,15 @@ def delete_media(
|
||||
- Delete database record
|
||||
- Return success/error
|
||||
"""
|
||||
return {
|
||||
"message": "Media deletion coming in Slice 3"
|
||||
}
|
||||
return {"message": "Media deletion coming in Slice 3"}
|
||||
|
||||
|
||||
@router.get("/{media_id}/usage")
|
||||
def get_media_usage(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get where this media file is being used.
|
||||
@@ -180,16 +175,16 @@ def get_media_usage(
|
||||
return {
|
||||
"products": [],
|
||||
"other_usage": [],
|
||||
"message": "Media usage tracking coming in Slice 3"
|
||||
"message": "Media usage tracking coming in Slice 3",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/optimize/{media_id}")
|
||||
def optimize_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Optimize media file (compress, resize, etc.).
|
||||
@@ -200,7 +195,4 @@ def optimize_media(
|
||||
- Keep original
|
||||
- Update database with new versions
|
||||
"""
|
||||
return {
|
||||
"message": "Media optimization coming in Slice 3"
|
||||
}
|
||||
|
||||
return {"message": "Media optimization coming in Slice 3"}
|
||||
|
||||
119
app/api/v1/vendor/notifications.py
vendored
119
app/api/v1/vendor/notifications.py
vendored
@@ -1,4 +1,4 @@
|
||||
# Notification management
|
||||
# Notification management
|
||||
# app/api/v1/vendor/notifications.py
|
||||
"""
|
||||
Vendor notification management endpoints.
|
||||
@@ -6,6 +6,7 @@ Vendor notification management endpoints.
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -21,12 +22,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
def get_notifications(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
unread_only: Optional[bool] = Query(False),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
unread_only: Optional[bool] = Query(False),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor notifications.
|
||||
@@ -41,15 +42,15 @@ def get_notifications(
|
||||
"notifications": [],
|
||||
"total": 0,
|
||||
"unread_count": 0,
|
||||
"message": "Notifications coming in Slice 5"
|
||||
"message": "Notifications coming in Slice 5",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get count of unread notifications.
|
||||
@@ -58,18 +59,15 @@ def get_unread_count(
|
||||
- Count unread notifications for vendor
|
||||
- Used for notification badge
|
||||
"""
|
||||
return {
|
||||
"unread_count": 0,
|
||||
"message": "Unread count coming in Slice 5"
|
||||
}
|
||||
return {"unread_count": 0, "message": "Unread count coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
def mark_as_read(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Mark notification as read.
|
||||
@@ -78,16 +76,14 @@ def mark_as_read(
|
||||
- Mark single notification as read
|
||||
- Update read timestamp
|
||||
"""
|
||||
return {
|
||||
"message": "Mark as read coming in Slice 5"
|
||||
}
|
||||
return {"message": "Mark as read coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/mark-all-read")
|
||||
def mark_all_as_read(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Mark all notifications as read.
|
||||
@@ -96,17 +92,15 @@ def mark_all_as_read(
|
||||
- Mark all vendor notifications as read
|
||||
- Update timestamps
|
||||
"""
|
||||
return {
|
||||
"message": "Mark all as read coming in Slice 5"
|
||||
}
|
||||
return {"message": "Mark all as read coming in Slice 5"}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete notification.
|
||||
@@ -115,16 +109,14 @@ def delete_notification(
|
||||
- Delete single notification
|
||||
- Verify notification belongs to vendor
|
||||
"""
|
||||
return {
|
||||
"message": "Notification deletion coming in Slice 5"
|
||||
}
|
||||
return {"message": "Notification deletion coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
def get_notification_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get notification preferences.
|
||||
@@ -138,16 +130,16 @@ def get_notification_settings(
|
||||
"email_notifications": True,
|
||||
"in_app_notifications": True,
|
||||
"notification_types": {},
|
||||
"message": "Notification settings coming in Slice 5"
|
||||
"message": "Notification settings coming in Slice 5",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings")
|
||||
def update_notification_settings(
|
||||
settings: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
settings: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update notification preferences.
|
||||
@@ -157,16 +149,14 @@ def update_notification_settings(
|
||||
- Update in-app notification settings
|
||||
- Enable/disable specific notification types
|
||||
"""
|
||||
return {
|
||||
"message": "Notification settings update coming in Slice 5"
|
||||
}
|
||||
return {"message": "Notification settings update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
def get_notification_templates(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get notification email templates.
|
||||
@@ -176,19 +166,16 @@ def get_notification_templates(
|
||||
- Include: order confirmation, shipping notification, etc.
|
||||
- Return template details
|
||||
"""
|
||||
return {
|
||||
"templates": [],
|
||||
"message": "Notification templates coming in Slice 5"
|
||||
}
|
||||
return {"templates": [], "message": "Notification templates coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}")
|
||||
def update_notification_template(
|
||||
template_id: int,
|
||||
template_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
template_id: int,
|
||||
template_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update notification email template.
|
||||
@@ -199,17 +186,15 @@ def update_notification_template(
|
||||
- Validate template variables
|
||||
- Preview template
|
||||
"""
|
||||
return {
|
||||
"message": "Template update coming in Slice 5"
|
||||
}
|
||||
return {"message": "Template update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
def send_test_notification(
|
||||
notification_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
notification_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Send test notification.
|
||||
@@ -219,6 +204,4 @@ def send_test_notification(
|
||||
- Use specified template
|
||||
- Send to current user's email
|
||||
"""
|
||||
return {
|
||||
"message": "Test notification coming in Slice 5"
|
||||
}
|
||||
return {"message": "Test notification coming in Slice 5"}
|
||||
|
||||
59
app/api/v1/vendor/orders.py
vendored
59
app/api/v1/vendor/orders.py
vendored
@@ -6,21 +6,17 @@ Vendor order management endpoints.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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.schema.order import (
|
||||
OrderResponse,
|
||||
OrderDetailResponse,
|
||||
OrderListResponse,
|
||||
OrderUpdate
|
||||
)
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor, VendorUser
|
||||
from models.schema.order import (OrderDetailResponse, OrderListResponse,
|
||||
OrderResponse, OrderUpdate)
|
||||
|
||||
router = APIRouter(prefix="/orders")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,13 +24,13 @@ 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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all orders for vendor.
|
||||
@@ -51,45 +47,41 @@ def get_vendor_orders(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
status=status,
|
||||
customer_id=customer_id
|
||||
customer_id=customer_id,
|
||||
)
|
||||
|
||||
return OrderListResponse(
|
||||
orders=[OrderResponse.model_validate(o) for o in orders],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
order_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed order information including items and addresses.
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
order = order_service.get_order(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_id=order_id
|
||||
)
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
order_id: int,
|
||||
order_update: OrderUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update order status and tracking information.
|
||||
@@ -105,10 +97,7 @@ def update_order_status(
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
order = order_service.update_order_status(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_id=order_id,
|
||||
order_update=order_update
|
||||
db=db, vendor_id=vendor.id, order_id=order_id, order_update=order_update
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
86
app/api/v1/vendor/payments.py
vendored
86
app/api/v1/vendor/payments.py
vendored
@@ -1,10 +1,11 @@
|
||||
# Payment configuration and processing
|
||||
# Payment configuration and processing
|
||||
# app/api/v1/vendor/payments.py
|
||||
"""
|
||||
Vendor payment configuration and processing endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -20,9 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/config")
|
||||
def get_payment_configuration(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor payment configuration.
|
||||
@@ -38,16 +39,16 @@ def get_payment_configuration(
|
||||
"accepted_methods": [],
|
||||
"currency": "EUR",
|
||||
"stripe_connected": False,
|
||||
"message": "Payment configuration coming in Slice 5"
|
||||
"message": "Payment configuration coming in Slice 5",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/config")
|
||||
def update_payment_configuration(
|
||||
payment_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
payment_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update vendor payment configuration.
|
||||
@@ -58,17 +59,15 @@ def update_payment_configuration(
|
||||
- Update accepted payment methods
|
||||
- Validate configuration before saving
|
||||
"""
|
||||
return {
|
||||
"message": "Payment configuration update coming in Slice 5"
|
||||
}
|
||||
return {"message": "Payment configuration update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.post("/stripe/connect")
|
||||
def connect_stripe_account(
|
||||
stripe_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
stripe_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Connect Stripe account for payment processing.
|
||||
@@ -79,16 +78,14 @@ def connect_stripe_account(
|
||||
- Verify Stripe account is active
|
||||
- Enable payment processing
|
||||
"""
|
||||
return {
|
||||
"message": "Stripe connection coming in Slice 5"
|
||||
}
|
||||
return {"message": "Stripe connection coming in Slice 5"}
|
||||
|
||||
|
||||
@router.delete("/stripe/disconnect")
|
||||
def disconnect_stripe_account(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Disconnect Stripe account.
|
||||
@@ -98,16 +95,14 @@ def disconnect_stripe_account(
|
||||
- Disable payment processing
|
||||
- Warn about pending payments
|
||||
"""
|
||||
return {
|
||||
"message": "Stripe disconnection coming in Slice 5"
|
||||
}
|
||||
return {"message": "Stripe disconnection coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/methods")
|
||||
def get_payment_methods(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get accepted payment methods for vendor.
|
||||
@@ -116,17 +111,14 @@ def get_payment_methods(
|
||||
- Return list of enabled payment methods
|
||||
- Include: credit card, PayPal, bank transfer, etc.
|
||||
"""
|
||||
return {
|
||||
"methods": [],
|
||||
"message": "Payment methods coming in Slice 5"
|
||||
}
|
||||
return {"methods": [], "message": "Payment methods coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
def get_payment_transactions(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get payment transaction history.
|
||||
@@ -140,15 +132,15 @@ def get_payment_transactions(
|
||||
return {
|
||||
"transactions": [],
|
||||
"total": 0,
|
||||
"message": "Payment transactions coming in Slice 5"
|
||||
"message": "Payment transactions coming in Slice 5",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/balance")
|
||||
def get_payment_balance(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor payment balance and payout information.
|
||||
@@ -164,17 +156,17 @@ def get_payment_balance(
|
||||
"pending_balance": 0.0,
|
||||
"currency": "EUR",
|
||||
"next_payout_date": None,
|
||||
"message": "Payment balance coming in Slice 5"
|
||||
"message": "Payment balance coming in Slice 5",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refund/{payment_id}")
|
||||
def refund_payment(
|
||||
payment_id: int,
|
||||
refund_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
payment_id: int,
|
||||
refund_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Process payment refund.
|
||||
@@ -185,6 +177,4 @@ def refund_payment(
|
||||
- Update order status
|
||||
- Send refund notification to customer
|
||||
"""
|
||||
return {
|
||||
"message": "Payment refund coming in Slice 5"
|
||||
}
|
||||
return {"message": "Payment refund coming in Slice 5"}
|
||||
|
||||
124
app/api/v1/vendor/products.py
vendored
124
app/api/v1/vendor/products.py
vendored
@@ -11,17 +11,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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.schema.product import (
|
||||
ProductCreate,
|
||||
ProductUpdate,
|
||||
ProductResponse,
|
||||
ProductDetailResponse,
|
||||
ProductListResponse
|
||||
)
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.product import (ProductCreate, ProductDetailResponse,
|
||||
ProductListResponse, ProductResponse,
|
||||
ProductUpdate)
|
||||
|
||||
router = APIRouter(prefix="/products")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,13 +25,13 @@ 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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all products in vendor catalog.
|
||||
@@ -50,29 +46,27 @@ def get_vendor_products(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=is_active,
|
||||
is_featured=is_featured
|
||||
is_featured=is_featured,
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=[ProductResponse.model_validate(p) for p in products],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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
|
||||
db=db, vendor_id=vendor.id, product_id=product_id
|
||||
)
|
||||
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
@@ -80,10 +74,10 @@ def get_product_details(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_data: ProductCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Add a product from marketplace to vendor catalog.
|
||||
@@ -91,9 +85,7 @@ def add_product_to_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
|
||||
db=db, vendor_id=vendor.id, product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -106,18 +98,15 @@ def add_product_to_catalog(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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
|
||||
db=db, vendor_id=vendor.id, product_id=product_id, product_update=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -130,17 +119,13 @@ def update_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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
product_service.delete_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_id=product_id
|
||||
)
|
||||
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} "
|
||||
@@ -152,10 +137,10 @@ def remove_product_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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
marketplace_product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Publish a marketplace product to vendor catalog.
|
||||
@@ -163,14 +148,11 @@ def publish_from_marketplace(
|
||||
Shortcut endpoint for publishing directly from marketplace import.
|
||||
"""
|
||||
product_data = ProductCreate(
|
||||
marketplace_product_id=marketplace_product_id,
|
||||
is_active=True
|
||||
marketplace_product_id=marketplace_product_id, is_active=True
|
||||
)
|
||||
|
||||
product = product_service.create_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_data=product_data
|
||||
db=db, vendor_id=vendor.id, product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -183,10 +165,10 @@ def publish_from_marketplace(
|
||||
|
||||
@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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product active status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
@@ -198,18 +180,15 @@ def toggle_product_active(
|
||||
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
|
||||
}
|
||||
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_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product featured status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
@@ -221,7 +200,4 @@ def toggle_product_featured(
|
||||
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
|
||||
}
|
||||
return {"message": f"Product {status}", "is_featured": product.is_featured}
|
||||
|
||||
19
app/api/v1/vendor/profile.py
vendored
19
app/api/v1/vendor/profile.py
vendored
@@ -4,16 +4,17 @@ Vendor profile management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.schema.vendor import VendorUpdate, VendorResponse
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.vendor import VendorResponse, VendorUpdate
|
||||
|
||||
router = APIRouter(prefix="/profile")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,9 +22,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("", response_model=VendorResponse)
|
||||
def get_vendor_profile(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
return vendor
|
||||
@@ -31,10 +32,10 @@ def get_vendor_profile(
|
||||
|
||||
@router.put("", response_model=VendorResponse)
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
# Verify user has permission to update vendor
|
||||
|
||||
17
app/api/v1/vendor/settings.py
vendored
17
app/api/v1/vendor/settings.py
vendored
@@ -4,13 +4,14 @@ Vendor settings and configuration endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
@@ -20,9 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
return {
|
||||
@@ -44,10 +45,10 @@ def get_vendor_settings(
|
||||
|
||||
@router.put("/marketplace")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Verify permissions
|
||||
|
||||
193
app/api/v1/vendor/team.py
vendored
193
app/api/v1/vendor/team.py
vendored
@@ -12,35 +12,24 @@ Implements complete team management with:
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import (get_current_vendor_api, get_user_permissions,
|
||||
require_vendor_owner, require_vendor_permission)
|
||||
from app.core.database import get_db
|
||||
from app.core.permissions import VendorPermissions
|
||||
from app.api.deps import (
|
||||
get_current_vendor_api,
|
||||
require_vendor_owner,
|
||||
require_vendor_permission,
|
||||
get_user_permissions
|
||||
)
|
||||
from app.services.vendor_team_service import vendor_team_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.team import (
|
||||
TeamMemberInvite,
|
||||
TeamMemberUpdate,
|
||||
TeamMemberResponse,
|
||||
TeamMemberListResponse,
|
||||
InvitationAccept,
|
||||
InvitationResponse,
|
||||
InvitationAcceptResponse,
|
||||
RoleResponse,
|
||||
RoleListResponse,
|
||||
UserPermissionsResponse,
|
||||
TeamStatistics,
|
||||
BulkRemoveRequest,
|
||||
BulkRemoveResponse,
|
||||
)
|
||||
from models.schema.team import (BulkRemoveRequest, BulkRemoveResponse,
|
||||
InvitationAccept, InvitationAcceptResponse,
|
||||
InvitationResponse, RoleListResponse,
|
||||
RoleResponse, TeamMemberInvite,
|
||||
TeamMemberListResponse, TeamMemberResponse,
|
||||
TeamMemberUpdate, TeamStatistics,
|
||||
UserPermissionsResponse)
|
||||
|
||||
router = APIRouter(prefix="/team")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -50,14 +39,15 @@ logger = logging.getLogger(__name__)
|
||||
# Team Member Routes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/members", response_model=TeamMemberListResponse)
|
||||
def list_team_members(
|
||||
request: Request,
|
||||
include_inactive: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_permission(
|
||||
VendorPermissions.TEAM_VIEW.value
|
||||
))
|
||||
request: Request,
|
||||
include_inactive: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(
|
||||
require_vendor_permission(VendorPermissions.TEAM_VIEW.value)
|
||||
),
|
||||
):
|
||||
"""
|
||||
Get all team members for current vendor.
|
||||
@@ -74,9 +64,7 @@ def list_team_members(
|
||||
vendor = request.state.vendor
|
||||
|
||||
members = vendor_team_service.get_team_members(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
include_inactive=include_inactive
|
||||
db=db, vendor=vendor, include_inactive=include_inactive
|
||||
)
|
||||
|
||||
# Calculate statistics
|
||||
@@ -90,19 +78,16 @@ def list_team_members(
|
||||
)
|
||||
|
||||
return TeamMemberListResponse(
|
||||
members=members,
|
||||
total=total,
|
||||
active_count=active,
|
||||
pending_invitations=pending
|
||||
members=members, total=total, active_count=active, pending_invitations=pending
|
||||
)
|
||||
|
||||
|
||||
@router.post("/invite", response_model=InvitationResponse)
|
||||
def invite_team_member(
|
||||
invitation: TeamMemberInvite,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner) # Owner only
|
||||
invitation: TeamMemberInvite,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner), # Owner only
|
||||
):
|
||||
"""
|
||||
Invite a new team member to the vendor.
|
||||
@@ -135,7 +120,7 @@ def invite_team_member(
|
||||
vendor=vendor,
|
||||
inviter=current_user,
|
||||
email=invitation.email,
|
||||
role_id=invitation.role_id
|
||||
role_id=invitation.role_id,
|
||||
)
|
||||
elif invitation.role_name:
|
||||
# Use role name with optional custom permissions
|
||||
@@ -145,7 +130,7 @@ def invite_team_member(
|
||||
inviter=current_user,
|
||||
email=invitation.email,
|
||||
role_name=invitation.role_name,
|
||||
custom_permissions=invitation.custom_permissions
|
||||
custom_permissions=invitation.custom_permissions,
|
||||
)
|
||||
else:
|
||||
# Default to Staff role
|
||||
@@ -154,7 +139,7 @@ def invite_team_member(
|
||||
vendor=vendor,
|
||||
inviter=current_user,
|
||||
email=invitation.email,
|
||||
role_name="staff"
|
||||
role_name="staff",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -166,15 +151,12 @@ def invite_team_member(
|
||||
message="Invitation sent successfully",
|
||||
email=result["email"],
|
||||
role=result["role"],
|
||||
invitation_sent=True
|
||||
invitation_sent=True,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/accept-invitation", response_model=InvitationAcceptResponse)
|
||||
def accept_invitation(
|
||||
acceptance: InvitationAccept,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def accept_invitation(acceptance: InvitationAccept, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Accept a team invitation and activate account.
|
||||
|
||||
@@ -196,7 +178,7 @@ def accept_invitation(
|
||||
invitation_token=acceptance.invitation_token,
|
||||
password=acceptance.password,
|
||||
first_name=acceptance.first_name,
|
||||
last_name=acceptance.last_name
|
||||
last_name=acceptance.last_name,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -210,26 +192,26 @@ def accept_invitation(
|
||||
"id": result["vendor"].id,
|
||||
"vendor_code": result["vendor"].vendor_code,
|
||||
"name": result["vendor"].name,
|
||||
"subdomain": result["vendor"].subdomain
|
||||
"subdomain": result["vendor"].subdomain,
|
||||
},
|
||||
user={
|
||||
"id": result["user"].id,
|
||||
"email": result["user"].email,
|
||||
"username": result["user"].username,
|
||||
"full_name": result["user"].full_name
|
||||
"full_name": result["user"].full_name,
|
||||
},
|
||||
role=result["role"]
|
||||
role=result["role"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/members/{user_id}", response_model=TeamMemberResponse)
|
||||
def get_team_member(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_permission(
|
||||
VendorPermissions.TEAM_VIEW.value
|
||||
))
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(
|
||||
require_vendor_permission(VendorPermissions.TEAM_VIEW.value)
|
||||
),
|
||||
):
|
||||
"""
|
||||
Get details of a specific team member.
|
||||
@@ -239,14 +221,13 @@ def get_team_member(
|
||||
vendor = request.state.vendor
|
||||
|
||||
members = vendor_team_service.get_team_members(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
include_inactive=True
|
||||
db=db, vendor=vendor, include_inactive=True
|
||||
)
|
||||
|
||||
member = next((m for m in members if m["id"] == user_id), None)
|
||||
if not member:
|
||||
from app.exceptions import UserNotFoundException
|
||||
|
||||
raise UserNotFoundException(str(user_id))
|
||||
|
||||
return TeamMemberResponse(**member)
|
||||
@@ -254,11 +235,11 @@ def get_team_member(
|
||||
|
||||
@router.put("/members/{user_id}", response_model=TeamMemberResponse)
|
||||
def update_team_member(
|
||||
user_id: int,
|
||||
update_data: TeamMemberUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner) # Owner only
|
||||
user_id: int,
|
||||
update_data: TeamMemberUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner), # Owner only
|
||||
):
|
||||
"""
|
||||
Update a team member's role or status.
|
||||
@@ -280,7 +261,7 @@ def update_team_member(
|
||||
vendor=vendor,
|
||||
user_id=user_id,
|
||||
new_role_id=update_data.role_id,
|
||||
is_active=update_data.is_active
|
||||
is_active=update_data.is_active,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -297,10 +278,10 @@ def update_team_member(
|
||||
|
||||
@router.delete("/members/{user_id}")
|
||||
def remove_team_member(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner) # Owner only
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner), # Owner only
|
||||
):
|
||||
"""
|
||||
Remove a team member from the vendor.
|
||||
@@ -316,29 +297,22 @@ def remove_team_member(
|
||||
"""
|
||||
vendor = request.state.vendor
|
||||
|
||||
vendor_team_service.remove_team_member(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
user_id=user_id
|
||||
)
|
||||
vendor_team_service.remove_team_member(db=db, vendor=vendor, user_id=user_id)
|
||||
|
||||
logger.info(
|
||||
f"Team member removed: {user_id} from {vendor.vendor_code} "
|
||||
f"by {current_user.username}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Team member removed successfully",
|
||||
"user_id": user_id
|
||||
}
|
||||
return {"message": "Team member removed successfully", "user_id": user_id}
|
||||
|
||||
|
||||
@router.post("/members/bulk-remove", response_model=BulkRemoveResponse)
|
||||
def bulk_remove_team_members(
|
||||
bulk_remove: BulkRemoveRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner)
|
||||
bulk_remove: BulkRemoveRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_owner),
|
||||
):
|
||||
"""
|
||||
Remove multiple team members at once.
|
||||
@@ -354,17 +328,12 @@ def bulk_remove_team_members(
|
||||
for user_id in bulk_remove.user_ids:
|
||||
try:
|
||||
vendor_team_service.remove_team_member(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
user_id=user_id
|
||||
db=db, vendor=vendor, user_id=user_id
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
errors.append({
|
||||
"user_id": user_id,
|
||||
"error": str(e)
|
||||
})
|
||||
errors.append({"user_id": user_id, "error": str(e)})
|
||||
|
||||
logger.info(
|
||||
f"Bulk remove completed: {success_count} removed, {failed_count} failed "
|
||||
@@ -372,9 +341,7 @@ def bulk_remove_team_members(
|
||||
)
|
||||
|
||||
return BulkRemoveResponse(
|
||||
success_count=success_count,
|
||||
failed_count=failed_count,
|
||||
errors=errors
|
||||
success_count=success_count, failed_count=failed_count, errors=errors
|
||||
)
|
||||
|
||||
|
||||
@@ -382,13 +349,14 @@ def bulk_remove_team_members(
|
||||
# Role Management Routes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/roles", response_model=RoleListResponse)
|
||||
def list_roles(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_permission(
|
||||
VendorPermissions.TEAM_VIEW.value
|
||||
))
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(
|
||||
require_vendor_permission(VendorPermissions.TEAM_VIEW.value)
|
||||
),
|
||||
):
|
||||
"""
|
||||
Get all available roles for the vendor.
|
||||
@@ -403,21 +371,19 @@ def list_roles(
|
||||
|
||||
roles = vendor_team_service.get_vendor_roles(db=db, vendor_id=vendor.id)
|
||||
|
||||
return RoleListResponse(
|
||||
roles=roles,
|
||||
total=len(roles)
|
||||
)
|
||||
return RoleListResponse(roles=roles, total=len(roles))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Permission Routes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/me/permissions", response_model=UserPermissionsResponse)
|
||||
def get_my_permissions(
|
||||
request: Request,
|
||||
permissions: List[str] = Depends(get_user_permissions),
|
||||
current_user: User = Depends(get_current_vendor_api)
|
||||
request: Request,
|
||||
permissions: List[str] = Depends(get_user_permissions),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
):
|
||||
"""
|
||||
Get current user's permissions in this vendor.
|
||||
@@ -443,7 +409,7 @@ def get_my_permissions(
|
||||
permissions=permissions,
|
||||
permission_count=len(permissions),
|
||||
is_owner=is_owner,
|
||||
role_name=role_name
|
||||
role_name=role_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -451,13 +417,14 @@ def get_my_permissions(
|
||||
# Statistics Routes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=TeamStatistics)
|
||||
def get_team_statistics(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_vendor_permission(
|
||||
VendorPermissions.TEAM_VIEW.value
|
||||
))
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(
|
||||
require_vendor_permission(VendorPermissions.TEAM_VIEW.value)
|
||||
),
|
||||
):
|
||||
"""
|
||||
Get team statistics for the vendor.
|
||||
@@ -474,9 +441,7 @@ def get_team_statistics(
|
||||
vendor = request.state.vendor
|
||||
|
||||
members = vendor_team_service.get_team_members(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
include_inactive=True
|
||||
db=db, vendor=vendor, include_inactive=True
|
||||
)
|
||||
|
||||
# Calculate statistics
|
||||
@@ -500,5 +465,5 @@ def get_team_statistics(
|
||||
pending_invitations=pending,
|
||||
owners=owners,
|
||||
team_members=team_members,
|
||||
roles_breakdown=roles_breakdown
|
||||
roles_breakdown=roles_breakdown,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user