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,
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ This module focuses purely on configuration storage and validation.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
@@ -137,13 +138,9 @@ settings = Settings()
|
||||
# ENVIRONMENT UTILITIES - Module-level functions
|
||||
# =============================================================================
|
||||
# Import environment detection utilities
|
||||
from app.core.environment import (
|
||||
get_environment,
|
||||
is_development,
|
||||
is_production,
|
||||
is_staging,
|
||||
should_use_secure_cookies
|
||||
)
|
||||
from app.core.environment import (get_environment, is_development,
|
||||
is_production, is_staging,
|
||||
should_use_secure_cookies)
|
||||
|
||||
|
||||
def get_current_environment() -> str:
|
||||
@@ -190,6 +187,7 @@ def is_staging_environment() -> bool:
|
||||
# VALIDATION FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def validate_production_settings() -> List[str]:
|
||||
"""
|
||||
Validate settings for production environment.
|
||||
@@ -243,22 +241,19 @@ def print_environment_info():
|
||||
# =============================================================================
|
||||
__all__ = [
|
||||
# Settings singleton
|
||||
'settings',
|
||||
|
||||
"settings",
|
||||
# Environment detection (re-exported from app.core.environment)
|
||||
'get_environment',
|
||||
'is_development',
|
||||
'is_production',
|
||||
'is_staging',
|
||||
'should_use_secure_cookies',
|
||||
|
||||
"get_environment",
|
||||
"is_development",
|
||||
"is_production",
|
||||
"is_staging",
|
||||
"should_use_secure_cookies",
|
||||
# Convenience functions
|
||||
'get_current_environment',
|
||||
'is_production_environment',
|
||||
'is_development_environment',
|
||||
'is_staging_environment',
|
||||
|
||||
"get_current_environment",
|
||||
"is_production_environment",
|
||||
"is_development_environment",
|
||||
"is_staging_environment",
|
||||
# Validation
|
||||
'validate_production_settings',
|
||||
'print_environment_info',
|
||||
"validate_production_settings",
|
||||
"print_environment_info",
|
||||
]
|
||||
|
||||
@@ -15,7 +15,7 @@ EnvironmentType = Literal["development", "staging", "production"]
|
||||
def get_environment() -> EnvironmentType:
|
||||
"""
|
||||
Detect current environment automatically.
|
||||
|
||||
|
||||
Detection logic:
|
||||
1. Check ENV environment variable if set
|
||||
2. Check ENVIRONMENT environment variable if set
|
||||
@@ -23,7 +23,7 @@ def get_environment() -> EnvironmentType:
|
||||
- localhost, 127.0.0.1 → development
|
||||
- Contains 'staging' → staging
|
||||
- Otherwise → production (safe default)
|
||||
|
||||
|
||||
Returns:
|
||||
str: 'development', 'staging', or 'production'
|
||||
"""
|
||||
@@ -35,7 +35,7 @@ def get_environment() -> EnvironmentType:
|
||||
return "staging"
|
||||
elif env in ["production", "prod"]:
|
||||
return "production"
|
||||
|
||||
|
||||
# Priority 2: ENVIRONMENT variable
|
||||
env = os.getenv("ENVIRONMENT", "").lower()
|
||||
if env in ["development", "dev", "local"]:
|
||||
@@ -44,22 +44,25 @@ def get_environment() -> EnvironmentType:
|
||||
return "staging"
|
||||
elif env in ["production", "prod"]:
|
||||
return "production"
|
||||
|
||||
|
||||
# Priority 3: Auto-detect from common indicators
|
||||
|
||||
|
||||
# Check if running in debug mode (common in development)
|
||||
if os.getenv("DEBUG", "").lower() in ["true", "1", "yes"]:
|
||||
return "development"
|
||||
|
||||
|
||||
# Check common development indicators
|
||||
hostname = os.getenv("HOSTNAME", "").lower()
|
||||
if any(dev_indicator in hostname for dev_indicator in ["local", "dev", "laptop", "desktop"]):
|
||||
if any(
|
||||
dev_indicator in hostname
|
||||
for dev_indicator in ["local", "dev", "laptop", "desktop"]
|
||||
):
|
||||
return "development"
|
||||
|
||||
|
||||
# Check for staging indicators
|
||||
if "staging" in hostname or "stage" in hostname:
|
||||
return "staging"
|
||||
|
||||
|
||||
# Default to development for safety (HTTPS not required in dev)
|
||||
# Change this to "production" if you prefer secure-by-default
|
||||
return "development"
|
||||
@@ -83,7 +86,7 @@ def is_production() -> bool:
|
||||
def should_use_secure_cookies() -> bool:
|
||||
"""
|
||||
Determine if cookies should have secure flag (HTTPS only).
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if production or staging, False if development
|
||||
"""
|
||||
@@ -97,7 +100,7 @@ _cached_environment: EnvironmentType | None = None
|
||||
def get_cached_environment() -> EnvironmentType:
|
||||
"""
|
||||
Get environment with caching.
|
||||
|
||||
|
||||
Environment is detected once and cached for performance.
|
||||
Useful if you call this frequently.
|
||||
"""
|
||||
|
||||
@@ -15,11 +15,13 @@ from fastapi import FastAPI
|
||||
from sqlalchemy import text
|
||||
|
||||
from middleware.auth import AuthManager
|
||||
# Remove this import if not needed: from models.database.base import Base
|
||||
|
||||
from .database import SessionLocal, engine
|
||||
from .logging import setup_logging
|
||||
|
||||
# Remove this import if not needed: from models.database.base import Base
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
auth_manager = AuthManager()
|
||||
|
||||
@@ -46,7 +48,9 @@ def check_database_ready():
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
# Try to query a table that should exist
|
||||
result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1"))
|
||||
result = conn.execute(
|
||||
text("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
|
||||
)
|
||||
tables = result.fetchall()
|
||||
return len(tables) > 0
|
||||
except Exception:
|
||||
@@ -93,6 +97,7 @@ def verify_startup_requirements():
|
||||
logger.info("[OK] Startup verification passed")
|
||||
return True
|
||||
|
||||
|
||||
# You can call this in your main.py if desired:
|
||||
# if not verify_startup_requirements():
|
||||
# raise RuntimeError("Application startup requirements not met")
|
||||
|
||||
@@ -17,6 +17,7 @@ class VendorPermissions(str, Enum):
|
||||
|
||||
Naming convention: RESOURCE_ACTION
|
||||
"""
|
||||
|
||||
# Dashboard
|
||||
DASHBOARD_VIEW = "dashboard.view"
|
||||
|
||||
@@ -166,17 +167,23 @@ class PermissionChecker:
|
||||
return required_permission in permissions
|
||||
|
||||
@staticmethod
|
||||
def has_any_permission(permissions: List[str], required_permissions: List[str]) -> bool:
|
||||
def has_any_permission(
|
||||
permissions: List[str], required_permissions: List[str]
|
||||
) -> bool:
|
||||
"""Check if a permission list contains ANY of the required permissions."""
|
||||
return any(perm in permissions for perm in required_permissions)
|
||||
|
||||
@staticmethod
|
||||
def has_all_permissions(permissions: List[str], required_permissions: List[str]) -> bool:
|
||||
def has_all_permissions(
|
||||
permissions: List[str], required_permissions: List[str]
|
||||
) -> bool:
|
||||
"""Check if a permission list contains ALL of the required permissions."""
|
||||
return all(perm in permissions for perm in required_permissions)
|
||||
|
||||
@staticmethod
|
||||
def get_missing_permissions(permissions: List[str], required_permissions: List[str]) -> List[str]:
|
||||
def get_missing_permissions(
|
||||
permissions: List[str], required_permissions: List[str]
|
||||
) -> List[str]:
|
||||
"""Get list of missing permissions."""
|
||||
return [perm for perm in required_permissions if perm not in permissions]
|
||||
|
||||
|
||||
@@ -16,19 +16,11 @@ THEME_PRESETS = {
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb" # Gray-200
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
}
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
|
||||
"modern": {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Indigo - Modern tech look
|
||||
@@ -36,19 +28,11 @@ THEME_PRESETS = {
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb" # Gray-200
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
}
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af", # Dark blue - Traditional
|
||||
@@ -56,19 +40,11 @@ THEME_PRESETS = {
|
||||
"accent": "#dc2626", # Red
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1d5db" # Gray-300
|
||||
"border": "#d1d5db", # Gray-300
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Georgia, serif",
|
||||
"body": "Arial, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "list",
|
||||
"header": "static",
|
||||
"product_card": "classic"
|
||||
}
|
||||
"fonts": {"heading": "Georgia, serif", "body": "Arial, sans-serif"},
|
||||
"layout": {"style": "list", "header": "static", "product_card": "classic"},
|
||||
},
|
||||
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000", # Black - Ultra minimal
|
||||
@@ -76,19 +52,11 @@ THEME_PRESETS = {
|
||||
"accent": "#666666", # Medium gray
|
||||
"background": "#ffffff", # White
|
||||
"text": "#000000", # Black
|
||||
"border": "#e5e7eb" # Light gray
|
||||
"border": "#e5e7eb", # Light gray
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Helvetica, sans-serif",
|
||||
"body": "Helvetica, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "transparent",
|
||||
"product_card": "minimal"
|
||||
}
|
||||
"fonts": {"heading": "Helvetica, sans-serif", "body": "Helvetica, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "transparent", "product_card": "minimal"},
|
||||
},
|
||||
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b", # Orange - Bold & energetic
|
||||
@@ -96,19 +64,11 @@ THEME_PRESETS = {
|
||||
"accent": "#8b5cf6", # Purple
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#fbbf24" # Yellow
|
||||
"border": "#fbbf24", # Yellow
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Poppins, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
}
|
||||
"fonts": {"heading": "Poppins, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "masonry", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
|
||||
"elegant": {
|
||||
"colors": {
|
||||
"primary": "#6b7280", # Gray - Sophisticated
|
||||
@@ -116,19 +76,11 @@ THEME_PRESETS = {
|
||||
"accent": "#d97706", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb" # Gray-200
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Playfair Display, serif",
|
||||
"body": "Lato, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "classic"
|
||||
}
|
||||
"fonts": {"heading": "Playfair Display, serif", "body": "Lato, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "classic"},
|
||||
},
|
||||
|
||||
"nature": {
|
||||
"colors": {
|
||||
"primary": "#059669", # Green - Natural & eco
|
||||
@@ -136,18 +88,11 @@ THEME_PRESETS = {
|
||||
"accent": "#f59e0b", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1fae5" # Light green
|
||||
"border": "#d1fae5", # Light green
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Montserrat, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
}
|
||||
}
|
||||
"fonts": {"heading": "Montserrat, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +188,7 @@ def get_preset_preview(preset_name: str) -> dict:
|
||||
"minimal": "Ultra-clean black and white aesthetic",
|
||||
"vibrant": "Bold and energetic with bright accent colors",
|
||||
"elegant": "Sophisticated gray tones with refined typography",
|
||||
"nature": "Fresh and eco-friendly green color palette"
|
||||
"nature": "Fresh and eco-friendly green color palette",
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -259,10 +204,7 @@ def get_preset_preview(preset_name: str) -> dict:
|
||||
|
||||
|
||||
def create_custom_preset(
|
||||
colors: dict,
|
||||
fonts: dict,
|
||||
layout: dict,
|
||||
name: str = "custom"
|
||||
colors: dict, fonts: dict, layout: dict, name: str = "custom"
|
||||
) -> dict:
|
||||
"""
|
||||
Create a custom preset from provided settings.
|
||||
@@ -304,8 +246,4 @@ def create_custom_preset(
|
||||
if "product_card" not in layout:
|
||||
layout["product_card"] = "modern"
|
||||
|
||||
return {
|
||||
"colors": colors,
|
||||
"fonts": fonts,
|
||||
"layout": layout
|
||||
}
|
||||
return {"colors": colors, "fonts": fonts, "layout": layout}
|
||||
|
||||
@@ -6,179 +6,109 @@ This module provides frontend-friendly exceptions with consistent error codes,
|
||||
messages, and HTTP status mappings.
|
||||
"""
|
||||
|
||||
# Base exceptions
|
||||
from .base import (
|
||||
WizamartException,
|
||||
ValidationException,
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
BusinessLogicException,
|
||||
ExternalServiceException,
|
||||
RateLimitException,
|
||||
ServiceUnavailableException,
|
||||
)
|
||||
|
||||
# Authentication exceptions
|
||||
from .auth import (
|
||||
InvalidCredentialsException,
|
||||
TokenExpiredException,
|
||||
InvalidTokenException,
|
||||
InsufficientPermissionsException,
|
||||
UserNotActiveException,
|
||||
AdminRequiredException,
|
||||
UserAlreadyExistsException
|
||||
)
|
||||
|
||||
# Admin exceptions
|
||||
from .admin import (
|
||||
UserNotFoundException,
|
||||
UserStatusChangeException,
|
||||
VendorVerificationException,
|
||||
AdminOperationException,
|
||||
CannotModifyAdminException,
|
||||
CannotModifySelfException,
|
||||
InvalidAdminActionException,
|
||||
BulkOperationException,
|
||||
ConfirmationRequiredException,
|
||||
)
|
||||
|
||||
# Marketplace import job exceptions
|
||||
from .marketplace_import_job import (
|
||||
MarketplaceImportException,
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException,
|
||||
InvalidImportDataException,
|
||||
ImportJobCannotBeCancelledException,
|
||||
ImportJobCannotBeDeletedException,
|
||||
MarketplaceConnectionException,
|
||||
MarketplaceDataParsingException,
|
||||
ImportRateLimitException,
|
||||
InvalidMarketplaceException,
|
||||
ImportJobAlreadyProcessingException,
|
||||
)
|
||||
|
||||
# Marketplace product exceptions
|
||||
from .marketplace_product import (
|
||||
MarketplaceProductNotFoundException,
|
||||
MarketplaceProductAlreadyExistsException,
|
||||
InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductValidationException,
|
||||
InvalidGTINException,
|
||||
MarketplaceProductCSVImportException,
|
||||
)
|
||||
|
||||
# Inventory exceptions
|
||||
from .inventory import (
|
||||
InventoryNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
InventoryValidationException,
|
||||
NegativeInventoryException,
|
||||
InvalidQuantityException,
|
||||
LocationNotFoundException
|
||||
)
|
||||
|
||||
# Vendor exceptions
|
||||
from .vendor import (
|
||||
VendorNotFoundException,
|
||||
VendorAlreadyExistsException,
|
||||
VendorNotActiveException,
|
||||
VendorNotVerifiedException,
|
||||
UnauthorizedVendorAccessException,
|
||||
InvalidVendorDataException,
|
||||
MaxVendorsReachedException,
|
||||
VendorValidationException,
|
||||
)
|
||||
|
||||
# Vendor domain exceptions
|
||||
from .vendor_domain import (
|
||||
VendorDomainNotFoundException,
|
||||
VendorDomainAlreadyExistsException,
|
||||
InvalidDomainFormatException,
|
||||
ReservedDomainException,
|
||||
DomainNotVerifiedException,
|
||||
DomainVerificationFailedException,
|
||||
DomainAlreadyVerifiedException,
|
||||
MultiplePrimaryDomainsException,
|
||||
DNSVerificationException,
|
||||
MaxDomainsReachedException,
|
||||
UnauthorizedDomainAccessException,
|
||||
)
|
||||
|
||||
# Vendor theme exceptions
|
||||
from .vendor_theme import (
|
||||
VendorThemeNotFoundException,
|
||||
InvalidThemeDataException,
|
||||
ThemePresetNotFoundException,
|
||||
ThemeValidationException,
|
||||
ThemePresetAlreadyAppliedException,
|
||||
InvalidColorFormatException,
|
||||
InvalidFontFamilyException,
|
||||
ThemeOperationException,
|
||||
)
|
||||
|
||||
# Customer exceptions
|
||||
from .customer import (
|
||||
CustomerNotFoundException,
|
||||
CustomerAlreadyExistsException,
|
||||
DuplicateCustomerEmailException,
|
||||
CustomerNotActiveException,
|
||||
InvalidCustomerCredentialsException,
|
||||
CustomerValidationException,
|
||||
CustomerAuthorizationException,
|
||||
)
|
||||
|
||||
# Team exceptions
|
||||
from .team import (
|
||||
TeamMemberNotFoundException,
|
||||
TeamMemberAlreadyExistsException,
|
||||
TeamInvitationNotFoundException,
|
||||
TeamInvitationExpiredException,
|
||||
TeamInvitationAlreadyAcceptedException,
|
||||
UnauthorizedTeamActionException,
|
||||
CannotRemoveOwnerException,
|
||||
CannotModifyOwnRoleException,
|
||||
RoleNotFoundException,
|
||||
InvalidRoleException,
|
||||
InsufficientTeamPermissionsException,
|
||||
MaxTeamMembersReachedException,
|
||||
TeamValidationException,
|
||||
InvalidInvitationDataException,
|
||||
InvalidInvitationTokenException,
|
||||
)
|
||||
|
||||
# Product exceptions
|
||||
from .product import (
|
||||
ProductNotFoundException,
|
||||
ProductAlreadyExistsException,
|
||||
ProductNotInCatalogException,
|
||||
ProductNotActiveException,
|
||||
InvalidProductDataException,
|
||||
ProductValidationException,
|
||||
CannotDeleteProductWithInventoryException,
|
||||
CannotDeleteProductWithOrdersException,
|
||||
)
|
||||
|
||||
# Order exceptions
|
||||
from .order import (
|
||||
OrderNotFoundException,
|
||||
OrderAlreadyExistsException,
|
||||
OrderValidationException,
|
||||
InvalidOrderStatusException,
|
||||
OrderCannotBeCancelledException,
|
||||
)
|
||||
|
||||
from .admin import (AdminOperationException, BulkOperationException,
|
||||
CannotModifyAdminException, CannotModifySelfException,
|
||||
ConfirmationRequiredException, InvalidAdminActionException,
|
||||
UserNotFoundException, UserStatusChangeException,
|
||||
VendorVerificationException)
|
||||
# Authentication exceptions
|
||||
from .auth import (AdminRequiredException, InsufficientPermissionsException,
|
||||
InvalidCredentialsException, InvalidTokenException,
|
||||
TokenExpiredException, UserAlreadyExistsException,
|
||||
UserNotActiveException)
|
||||
# Base exceptions
|
||||
from .base import (AuthenticationException, AuthorizationException,
|
||||
BusinessLogicException, ConflictException,
|
||||
ExternalServiceException, RateLimitException,
|
||||
ResourceNotFoundException, ServiceUnavailableException,
|
||||
ValidationException, WizamartException)
|
||||
# Cart exceptions
|
||||
from .cart import (
|
||||
CartItemNotFoundException,
|
||||
EmptyCartException,
|
||||
CartValidationException,
|
||||
InsufficientInventoryForCartException,
|
||||
InvalidCartQuantityException,
|
||||
ProductNotAvailableForCartException,
|
||||
)
|
||||
from .cart import (CartItemNotFoundException, CartValidationException,
|
||||
EmptyCartException, InsufficientInventoryForCartException,
|
||||
InvalidCartQuantityException,
|
||||
ProductNotAvailableForCartException)
|
||||
# Customer exceptions
|
||||
from .customer import (CustomerAlreadyExistsException,
|
||||
CustomerAuthorizationException,
|
||||
CustomerNotActiveException, CustomerNotFoundException,
|
||||
CustomerValidationException,
|
||||
DuplicateCustomerEmailException,
|
||||
InvalidCustomerCredentialsException)
|
||||
# Inventory exceptions
|
||||
from .inventory import (InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
InvalidQuantityException, InventoryNotFoundException,
|
||||
InventoryValidationException,
|
||||
LocationNotFoundException, NegativeInventoryException)
|
||||
# Marketplace import job exceptions
|
||||
from .marketplace_import_job import (ImportJobAlreadyProcessingException,
|
||||
ImportJobCannotBeCancelledException,
|
||||
ImportJobCannotBeDeletedException,
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException,
|
||||
ImportRateLimitException,
|
||||
InvalidImportDataException,
|
||||
InvalidMarketplaceException,
|
||||
MarketplaceConnectionException,
|
||||
MarketplaceDataParsingException,
|
||||
MarketplaceImportException)
|
||||
# Marketplace product exceptions
|
||||
from .marketplace_product import (InvalidGTINException,
|
||||
InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductAlreadyExistsException,
|
||||
MarketplaceProductCSVImportException,
|
||||
MarketplaceProductNotFoundException,
|
||||
MarketplaceProductValidationException)
|
||||
# Order exceptions
|
||||
from .order import (InvalidOrderStatusException, OrderAlreadyExistsException,
|
||||
OrderCannotBeCancelledException, OrderNotFoundException,
|
||||
OrderValidationException)
|
||||
# Product exceptions
|
||||
from .product import (CannotDeleteProductWithInventoryException,
|
||||
CannotDeleteProductWithOrdersException,
|
||||
InvalidProductDataException,
|
||||
ProductAlreadyExistsException, ProductNotActiveException,
|
||||
ProductNotFoundException, ProductNotInCatalogException,
|
||||
ProductValidationException)
|
||||
# Team exceptions
|
||||
from .team import (CannotModifyOwnRoleException, CannotRemoveOwnerException,
|
||||
InsufficientTeamPermissionsException,
|
||||
InvalidInvitationDataException,
|
||||
InvalidInvitationTokenException, InvalidRoleException,
|
||||
MaxTeamMembersReachedException, RoleNotFoundException,
|
||||
TeamInvitationAlreadyAcceptedException,
|
||||
TeamInvitationExpiredException,
|
||||
TeamInvitationNotFoundException,
|
||||
TeamMemberAlreadyExistsException,
|
||||
TeamMemberNotFoundException, TeamValidationException,
|
||||
UnauthorizedTeamActionException)
|
||||
# Vendor exceptions
|
||||
from .vendor import (InvalidVendorDataException, MaxVendorsReachedException,
|
||||
UnauthorizedVendorAccessException,
|
||||
VendorAlreadyExistsException, VendorNotActiveException,
|
||||
VendorNotFoundException, VendorNotVerifiedException,
|
||||
VendorValidationException)
|
||||
# Vendor domain exceptions
|
||||
from .vendor_domain import (DNSVerificationException,
|
||||
DomainAlreadyVerifiedException,
|
||||
DomainNotVerifiedException,
|
||||
DomainVerificationFailedException,
|
||||
InvalidDomainFormatException,
|
||||
MaxDomainsReachedException,
|
||||
MultiplePrimaryDomainsException,
|
||||
ReservedDomainException,
|
||||
UnauthorizedDomainAccessException,
|
||||
VendorDomainAlreadyExistsException,
|
||||
VendorDomainNotFoundException)
|
||||
# Vendor theme exceptions
|
||||
from .vendor_theme import (InvalidColorFormatException,
|
||||
InvalidFontFamilyException,
|
||||
InvalidThemeDataException, ThemeOperationException,
|
||||
ThemePresetAlreadyAppliedException,
|
||||
ThemePresetNotFoundException,
|
||||
ThemeValidationException,
|
||||
VendorThemeNotFoundException)
|
||||
|
||||
__all__ = [
|
||||
# Base exceptions
|
||||
@@ -192,7 +122,6 @@ __all__ = [
|
||||
"ExternalServiceException",
|
||||
"RateLimitException",
|
||||
"ServiceUnavailableException",
|
||||
|
||||
# Auth exceptions
|
||||
"InvalidCredentialsException",
|
||||
"TokenExpiredException",
|
||||
@@ -201,7 +130,6 @@ __all__ = [
|
||||
"UserNotActiveException",
|
||||
"AdminRequiredException",
|
||||
"UserAlreadyExistsException",
|
||||
|
||||
# Customer exceptions
|
||||
"CustomerNotFoundException",
|
||||
"CustomerAlreadyExistsException",
|
||||
@@ -210,7 +138,6 @@ __all__ = [
|
||||
"InvalidCustomerCredentialsException",
|
||||
"CustomerValidationException",
|
||||
"CustomerAuthorizationException",
|
||||
|
||||
# Team exceptions
|
||||
"TeamMemberNotFoundException",
|
||||
"TeamMemberAlreadyExistsException",
|
||||
@@ -227,7 +154,6 @@ __all__ = [
|
||||
"TeamValidationException",
|
||||
"InvalidInvitationDataException",
|
||||
"InvalidInvitationTokenException",
|
||||
|
||||
# Inventory exceptions
|
||||
"InventoryNotFoundException",
|
||||
"InsufficientInventoryException",
|
||||
@@ -236,7 +162,6 @@ __all__ = [
|
||||
"NegativeInventoryException",
|
||||
"InvalidQuantityException",
|
||||
"LocationNotFoundException",
|
||||
|
||||
# Vendor exceptions
|
||||
"VendorNotFoundException",
|
||||
"VendorAlreadyExistsException",
|
||||
@@ -246,7 +171,6 @@ __all__ = [
|
||||
"InvalidVendorDataException",
|
||||
"MaxVendorsReachedException",
|
||||
"VendorValidationException",
|
||||
|
||||
# Vendor Domain
|
||||
"VendorDomainNotFoundException",
|
||||
"VendorDomainAlreadyExistsException",
|
||||
@@ -259,7 +183,6 @@ __all__ = [
|
||||
"DNSVerificationException",
|
||||
"MaxDomainsReachedException",
|
||||
"UnauthorizedDomainAccessException",
|
||||
|
||||
# Vendor Theme
|
||||
"VendorThemeNotFoundException",
|
||||
"InvalidThemeDataException",
|
||||
@@ -269,7 +192,6 @@ __all__ = [
|
||||
"InvalidColorFormatException",
|
||||
"InvalidFontFamilyException",
|
||||
"ThemeOperationException",
|
||||
|
||||
# Product exceptions
|
||||
"ProductNotFoundException",
|
||||
"ProductAlreadyExistsException",
|
||||
@@ -279,14 +201,12 @@ __all__ = [
|
||||
"ProductValidationException",
|
||||
"CannotDeleteProductWithInventoryException",
|
||||
"CannotDeleteProductWithOrdersException",
|
||||
|
||||
# Order exceptions
|
||||
"OrderNotFoundException",
|
||||
"OrderAlreadyExistsException",
|
||||
"OrderValidationException",
|
||||
"InvalidOrderStatusException",
|
||||
"OrderCannotBeCancelledException",
|
||||
|
||||
# Cart exceptions
|
||||
"CartItemNotFoundException",
|
||||
"EmptyCartException",
|
||||
@@ -294,7 +214,6 @@ __all__ = [
|
||||
"InsufficientInventoryForCartException",
|
||||
"InvalidCartQuantityException",
|
||||
"ProductNotAvailableForCartException",
|
||||
|
||||
# MarketplaceProduct exceptions
|
||||
"MarketplaceProductNotFoundException",
|
||||
"MarketplaceProductAlreadyExistsException",
|
||||
@@ -302,7 +221,6 @@ __all__ = [
|
||||
"MarketplaceProductValidationException",
|
||||
"InvalidGTINException",
|
||||
"MarketplaceProductCSVImportException",
|
||||
|
||||
# Marketplace import exceptions
|
||||
"MarketplaceImportException",
|
||||
"ImportJobNotFoundException",
|
||||
@@ -315,7 +233,6 @@ __all__ = [
|
||||
"ImportRateLimitException",
|
||||
"InvalidMarketplaceException",
|
||||
"ImportJobAlreadyProcessingException",
|
||||
|
||||
# Admin exceptions
|
||||
"UserNotFoundException",
|
||||
"UserStatusChangeException",
|
||||
|
||||
@@ -4,12 +4,9 @@ Admin operations specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
BusinessLogicException,
|
||||
AuthorizationException,
|
||||
ValidationException
|
||||
)
|
||||
|
||||
from .base import (AuthorizationException, BusinessLogicException,
|
||||
ResourceNotFoundException, ValidationException)
|
||||
|
||||
|
||||
class UserNotFoundException(ResourceNotFoundException):
|
||||
@@ -35,11 +32,11 @@ class UserStatusChangeException(BusinessLogicException):
|
||||
"""Raised when user status cannot be changed."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: int,
|
||||
current_status: str,
|
||||
attempted_action: str,
|
||||
reason: Optional[str] = None,
|
||||
self,
|
||||
user_id: int,
|
||||
current_status: str,
|
||||
attempted_action: str,
|
||||
reason: Optional[str] = None,
|
||||
):
|
||||
message = f"Cannot {attempted_action} user {user_id} (current status: {current_status})"
|
||||
if reason:
|
||||
@@ -61,10 +58,10 @@ class ShopVerificationException(BusinessLogicException):
|
||||
"""Raised when shop verification fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
shop_id: int,
|
||||
reason: str,
|
||||
current_verification_status: Optional[bool] = None,
|
||||
self,
|
||||
shop_id: int,
|
||||
reason: str,
|
||||
current_verification_status: Optional[bool] = None,
|
||||
):
|
||||
details = {
|
||||
"shop_id": shop_id,
|
||||
@@ -85,11 +82,11 @@ class AdminOperationException(BusinessLogicException):
|
||||
"""Raised when admin operation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation: str,
|
||||
reason: str,
|
||||
target_type: Optional[str] = None,
|
||||
target_id: Optional[str] = None,
|
||||
self,
|
||||
operation: str,
|
||||
reason: str,
|
||||
target_type: Optional[str] = None,
|
||||
target_id: Optional[str] = None,
|
||||
):
|
||||
message = f"Admin operation '{operation}' failed: {reason}"
|
||||
|
||||
@@ -142,10 +139,10 @@ class InvalidAdminActionException(ValidationException):
|
||||
"""Raised when admin action is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
action: str,
|
||||
reason: str,
|
||||
valid_actions: Optional[list] = None,
|
||||
self,
|
||||
action: str,
|
||||
reason: str,
|
||||
valid_actions: Optional[list] = None,
|
||||
):
|
||||
details = {
|
||||
"action": action,
|
||||
@@ -166,11 +163,11 @@ class BulkOperationException(BusinessLogicException):
|
||||
"""Raised when bulk admin operation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation: str,
|
||||
total_items: int,
|
||||
failed_items: int,
|
||||
errors: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
operation: str,
|
||||
total_items: int,
|
||||
failed_items: int,
|
||||
errors: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
message = f"Bulk {operation} completed with errors: {failed_items}/{total_items} failed"
|
||||
|
||||
@@ -195,10 +192,10 @@ class ConfirmationRequiredException(BusinessLogicException):
|
||||
"""Raised when a destructive operation requires explicit confirmation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation: str,
|
||||
message: Optional[str] = None,
|
||||
confirmation_param: str = "confirm"
|
||||
self,
|
||||
operation: str,
|
||||
message: Optional[str] = None,
|
||||
confirmation_param: str = "confirm",
|
||||
):
|
||||
if not message:
|
||||
message = f"Operation '{operation}' requires confirmation parameter: {confirmation_param}=true"
|
||||
@@ -217,10 +214,10 @@ class VendorVerificationException(BusinessLogicException):
|
||||
"""Raised when vendor verification fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vendor_id: int,
|
||||
reason: str,
|
||||
current_verification_status: Optional[bool] = None,
|
||||
self,
|
||||
vendor_id: int,
|
||||
reason: str,
|
||||
current_verification_status: Optional[bool] = None,
|
||||
):
|
||||
details = {
|
||||
"vendor_id": vendor_id,
|
||||
|
||||
@@ -4,7 +4,9 @@ Authentication and authorization specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .base import AuthenticationException, AuthorizationException, ConflictException
|
||||
|
||||
from .base import (AuthenticationException, AuthorizationException,
|
||||
ConflictException)
|
||||
|
||||
|
||||
class InvalidCredentialsException(AuthenticationException):
|
||||
@@ -41,9 +43,9 @@ class InsufficientPermissionsException(AuthorizationException):
|
||||
"""Raised when user lacks required permissions for an action."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Insufficient permissions for this action",
|
||||
required_permission: Optional[str] = None,
|
||||
self,
|
||||
message: str = "Insufficient permissions for this action",
|
||||
required_permission: Optional[str] = None,
|
||||
):
|
||||
details = {}
|
||||
if required_permission:
|
||||
@@ -80,9 +82,9 @@ class UserAlreadyExistsException(ConflictException):
|
||||
"""Raised when trying to register with existing username/email."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "User already exists",
|
||||
field: Optional[str] = None,
|
||||
self,
|
||||
message: str = "User already exists",
|
||||
field: Optional[str] = None,
|
||||
):
|
||||
details = {}
|
||||
if field:
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Backup/recovery exceptions
|
||||
# Backup/recovery exceptions
|
||||
|
||||
@@ -39,8 +39,6 @@ class WizamartException(Exception):
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
class ValidationException(WizamartException):
|
||||
"""Raised when request validation fails."""
|
||||
|
||||
@@ -62,8 +60,6 @@ class ValidationException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class AuthenticationException(WizamartException):
|
||||
"""Raised when authentication fails."""
|
||||
|
||||
@@ -97,6 +93,7 @@ class AuthorizationException(WizamartException):
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class ResourceNotFoundException(WizamartException):
|
||||
"""Raised when a requested resource is not found."""
|
||||
|
||||
@@ -122,6 +119,7 @@ class ResourceNotFoundException(WizamartException):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ConflictException(WizamartException):
|
||||
"""Raised when a resource conflict occurs."""
|
||||
|
||||
@@ -138,6 +136,7 @@ class ConflictException(WizamartException):
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class BusinessLogicException(WizamartException):
|
||||
"""Raised when business logic rules are violated."""
|
||||
|
||||
@@ -196,6 +195,7 @@ class RateLimitException(WizamartException):
|
||||
details=rate_limit_details,
|
||||
)
|
||||
|
||||
|
||||
class ServiceUnavailableException(WizamartException):
|
||||
"""Raised when service is unavailable."""
|
||||
|
||||
@@ -206,6 +206,7 @@ class ServiceUnavailableException(WizamartException):
|
||||
status_code=503,
|
||||
)
|
||||
|
||||
|
||||
# Note: Domain-specific exceptions like VendorNotFoundException, UserNotFoundException, etc.
|
||||
# are defined in their respective domain modules (vendor.py, admin.py, etc.)
|
||||
# to keep domain-specific logic separate from base exceptions.
|
||||
|
||||
@@ -4,11 +4,9 @@ Shopping cart specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (BusinessLogicException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class CartItemNotFoundException(ResourceNotFoundException):
|
||||
@@ -19,22 +17,16 @@ class CartItemNotFoundException(ResourceNotFoundException):
|
||||
resource_type="CartItem",
|
||||
identifier=str(product_id),
|
||||
message=f"Product {product_id} not found in cart",
|
||||
error_code="CART_ITEM_NOT_FOUND"
|
||||
error_code="CART_ITEM_NOT_FOUND",
|
||||
)
|
||||
self.details.update({
|
||||
"product_id": product_id,
|
||||
"session_id": session_id
|
||||
})
|
||||
self.details.update({"product_id": product_id, "session_id": session_id})
|
||||
|
||||
|
||||
class EmptyCartException(ValidationException):
|
||||
"""Raised when trying to perform operations on an empty cart."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
super().__init__(
|
||||
message="Cart is empty",
|
||||
details={"session_id": session_id}
|
||||
)
|
||||
super().__init__(message="Cart is empty", details={"session_id": session_id})
|
||||
self.error_code = "CART_EMPTY"
|
||||
|
||||
|
||||
@@ -82,7 +74,9 @@ class InsufficientInventoryForCartException(BusinessLogicException):
|
||||
class InvalidCartQuantityException(ValidationException):
|
||||
"""Raised when cart quantity is invalid."""
|
||||
|
||||
def __init__(self, quantity: int, min_quantity: int = 1, max_quantity: Optional[int] = None):
|
||||
def __init__(
|
||||
self, quantity: int, min_quantity: int = 1, max_quantity: Optional[int] = None
|
||||
):
|
||||
if quantity < min_quantity:
|
||||
message = f"Quantity must be at least {min_quantity}"
|
||||
elif max_quantity and quantity > max_quantity:
|
||||
|
||||
@@ -4,13 +4,10 @@ Customer management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
AuthenticationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (AuthenticationException, BusinessLogicException,
|
||||
ConflictException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class CustomerNotFoundException(ResourceNotFoundException):
|
||||
@@ -21,7 +18,7 @@ class CustomerNotFoundException(ResourceNotFoundException):
|
||||
resource_type="Customer",
|
||||
identifier=customer_identifier,
|
||||
message=f"Customer '{customer_identifier}' not found",
|
||||
error_code="CUSTOMER_NOT_FOUND"
|
||||
error_code="CUSTOMER_NOT_FOUND",
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +29,7 @@ class CustomerAlreadyExistsException(ConflictException):
|
||||
super().__init__(
|
||||
message=f"Customer with email '{email}' already exists",
|
||||
error_code="CUSTOMER_ALREADY_EXISTS",
|
||||
details={"email": email}
|
||||
details={"email": email},
|
||||
)
|
||||
|
||||
|
||||
@@ -43,10 +40,7 @@ class DuplicateCustomerEmailException(ConflictException):
|
||||
super().__init__(
|
||||
message=f"Email '{email}' is already registered for this vendor",
|
||||
error_code="DUPLICATE_CUSTOMER_EMAIL",
|
||||
details={
|
||||
"email": email,
|
||||
"vendor_code": vendor_code
|
||||
}
|
||||
details={"email": email, "vendor_code": vendor_code},
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +51,7 @@ class CustomerNotActiveException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Customer account '{email}' is not active",
|
||||
error_code="CUSTOMER_NOT_ACTIVE",
|
||||
details={"email": email}
|
||||
details={"email": email},
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +61,7 @@ class InvalidCustomerCredentialsException(AuthenticationException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
message="Invalid email or password",
|
||||
error_code="INVALID_CUSTOMER_CREDENTIALS"
|
||||
error_code="INVALID_CUSTOMER_CREDENTIALS",
|
||||
)
|
||||
|
||||
|
||||
@@ -78,13 +72,9 @@ class CustomerValidationException(ValidationException):
|
||||
self,
|
||||
message: str = "Customer validation failed",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
field=field,
|
||||
details=details
|
||||
)
|
||||
super().__init__(message=message, field=field, details=details)
|
||||
self.error_code = "CUSTOMER_VALIDATION_FAILED"
|
||||
|
||||
|
||||
@@ -95,8 +85,5 @@ class CustomerAuthorizationException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Customer '{customer_email}' not authorized for: {operation}",
|
||||
error_code="CUSTOMER_NOT_AUTHORIZED",
|
||||
details={
|
||||
"customer_email": customer_email,
|
||||
"operation": operation
|
||||
}
|
||||
details={"customer_email": customer_email, "operation": operation},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ Handles fallback logic and context-specific customization.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
@@ -114,7 +114,7 @@ class ErrorPageRenderer:
|
||||
"error_code": error_code,
|
||||
"context": context_type.value,
|
||||
"template": template_path,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -129,8 +129,7 @@ class ErrorPageRenderer:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to render error template {template_path}: {e}",
|
||||
exc_info=True
|
||||
f"Failed to render error template {template_path}: {e}", exc_info=True
|
||||
)
|
||||
# Return basic HTML as absolute fallback
|
||||
return ErrorPageRenderer._render_basic_html_fallback(
|
||||
@@ -228,7 +227,9 @@ class ErrorPageRenderer:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_context_data(request: Request, context_type: RequestContext) -> Dict[str, Any]:
|
||||
def _get_context_data(
|
||||
request: Request, context_type: RequestContext
|
||||
) -> Dict[str, Any]:
|
||||
"""Get context-specific data for error templates."""
|
||||
data = {}
|
||||
|
||||
@@ -261,11 +262,19 @@ class ErrorPageRenderer:
|
||||
|
||||
# Calculate base_url for shop links
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
base_url = "/"
|
||||
if access_method == "path" and vendor:
|
||||
# Use the full_prefix from vendor_context to determine which pattern was used
|
||||
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/"
|
||||
)
|
||||
base_url = f"{full_prefix}{vendor.subdomain}/"
|
||||
data["base_url"] = base_url
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@ This module provides classes and functions for:
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
|
||||
from middleware.context import RequestContext, get_request_context
|
||||
|
||||
from .base import WizamartException
|
||||
from .error_renderer import ErrorPageRenderer
|
||||
from middleware.context import RequestContext, get_request_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,8 +39,8 @@ def setup_exception_handlers(app):
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"accept": request.headers.get("accept", ""),
|
||||
"method": request.method
|
||||
}
|
||||
"method": request.method,
|
||||
},
|
||||
)
|
||||
|
||||
# Redirect to appropriate login page based on context
|
||||
@@ -56,15 +57,12 @@ def setup_exception_handlers(app):
|
||||
"url": str(request.url),
|
||||
"method": request.method,
|
||||
"exception_type": type(exc).__name__,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an API request
|
||||
if _is_api_request(request):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=exc.to_dict()
|
||||
)
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
|
||||
|
||||
# Check if this is an HTML page request
|
||||
if _is_html_page_request(request):
|
||||
@@ -78,10 +76,7 @@ def setup_exception_handlers(app):
|
||||
)
|
||||
|
||||
# Default to JSON for unknown request types
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=exc.to_dict()
|
||||
)
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
@@ -96,7 +91,7 @@ def setup_exception_handlers(app):
|
||||
"url": str(request.url),
|
||||
"method": request.method,
|
||||
"exception_type": "HTTPException",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an API request
|
||||
@@ -107,7 +102,7 @@ def setup_exception_handlers(app):
|
||||
"error_code": f"HTTP_{exc.status_code}",
|
||||
"message": exc.detail,
|
||||
"status_code": exc.status_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an HTML page request
|
||||
@@ -128,11 +123,13 @@ def setup_exception_handlers(app):
|
||||
"error_code": f"HTTP_{exc.status_code}",
|
||||
"message": exc.detail,
|
||||
"status_code": exc.status_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
async def validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
):
|
||||
"""Handle Pydantic validation errors with consistent format."""
|
||||
|
||||
# Sanitize errors to remove sensitive data from logs
|
||||
@@ -140,8 +137,8 @@ def setup_exception_handlers(app):
|
||||
for error in exc.errors():
|
||||
sanitized_error = error.copy()
|
||||
# Remove 'input' field which may contain passwords
|
||||
if 'input' in sanitized_error:
|
||||
sanitized_error['input'] = '<redacted>'
|
||||
if "input" in sanitized_error:
|
||||
sanitized_error["input"] = "<redacted>"
|
||||
sanitized_errors.append(sanitized_error)
|
||||
|
||||
logger.error(
|
||||
@@ -151,7 +148,7 @@ def setup_exception_handlers(app):
|
||||
"url": str(request.url),
|
||||
"method": request.method,
|
||||
"exception_type": "RequestValidationError",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Clean up validation errors to ensure JSON serializability
|
||||
@@ -159,15 +156,17 @@ def setup_exception_handlers(app):
|
||||
for error in exc.errors():
|
||||
clean_error = {}
|
||||
for key, value in error.items():
|
||||
if key == 'input' and isinstance(value, bytes):
|
||||
if key == "input" and isinstance(value, bytes):
|
||||
# Convert bytes to string representation for JSON serialization
|
||||
clean_error[key] = f"<bytes: {len(value)} bytes>"
|
||||
elif key == 'ctx' and isinstance(value, dict):
|
||||
elif key == "ctx" and isinstance(value, dict):
|
||||
# Handle the 'ctx' field that contains ValueError objects
|
||||
clean_ctx = {}
|
||||
for ctx_key, ctx_value in value.items():
|
||||
if isinstance(ctx_value, Exception):
|
||||
clean_ctx[ctx_key] = str(ctx_value) # Convert exception to string
|
||||
clean_ctx[ctx_key] = str(
|
||||
ctx_value
|
||||
) # Convert exception to string
|
||||
else:
|
||||
clean_ctx[ctx_key] = ctx_value
|
||||
clean_error[key] = clean_ctx
|
||||
@@ -186,10 +185,8 @@ def setup_exception_handlers(app):
|
||||
"error_code": "VALIDATION_ERROR",
|
||||
"message": "Request validation failed",
|
||||
"status_code": 422,
|
||||
"details": {
|
||||
"validation_errors": clean_errors
|
||||
}
|
||||
}
|
||||
"details": {"validation_errors": clean_errors},
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an HTML page request
|
||||
@@ -210,10 +207,8 @@ def setup_exception_handlers(app):
|
||||
"error_code": "VALIDATION_ERROR",
|
||||
"message": "Request validation failed",
|
||||
"status_code": 422,
|
||||
"details": {
|
||||
"validation_errors": clean_errors
|
||||
}
|
||||
}
|
||||
"details": {"validation_errors": clean_errors},
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
@@ -227,7 +222,7 @@ def setup_exception_handlers(app):
|
||||
"url": str(request.url),
|
||||
"method": request.method,
|
||||
"exception_type": type(exc).__name__,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an API request
|
||||
@@ -238,7 +233,7 @@ def setup_exception_handlers(app):
|
||||
"error_code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Internal server error",
|
||||
"status_code": 500,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an HTML page request
|
||||
@@ -259,7 +254,7 @@ def setup_exception_handlers(app):
|
||||
"error_code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Internal server error",
|
||||
"status_code": 500,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@app.exception_handler(404)
|
||||
@@ -275,11 +270,8 @@ def setup_exception_handlers(app):
|
||||
"error_code": "ENDPOINT_NOT_FOUND",
|
||||
"message": f"Endpoint not found: {request.url.path}",
|
||||
"status_code": 404,
|
||||
"details": {
|
||||
"path": request.url.path,
|
||||
"method": request.method
|
||||
}
|
||||
}
|
||||
"details": {"path": request.url.path, "method": request.method},
|
||||
},
|
||||
)
|
||||
|
||||
# Check if this is an HTML page request
|
||||
@@ -300,11 +292,8 @@ def setup_exception_handlers(app):
|
||||
"error_code": "ENDPOINT_NOT_FOUND",
|
||||
"message": f"Endpoint not found: {request.url.path}",
|
||||
"status_code": 404,
|
||||
"details": {
|
||||
"path": request.url.path,
|
||||
"method": request.method
|
||||
}
|
||||
}
|
||||
"details": {"path": request.url.path, "method": request.method},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -332,8 +321,8 @@ def _is_html_page_request(request: Request) -> bool:
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"accept": request.headers.get("accept", "")
|
||||
}
|
||||
"accept": request.headers.get("accept", ""),
|
||||
},
|
||||
)
|
||||
|
||||
# Don't redirect API calls
|
||||
@@ -354,7 +343,9 @@ def _is_html_page_request(request: Request) -> bool:
|
||||
# MUST explicitly accept HTML (strict check)
|
||||
accept_header = request.headers.get("accept", "")
|
||||
if "text/html" not in accept_header:
|
||||
logger.debug(f"Not HTML page: Accept header doesn't include text/html: {accept_header}")
|
||||
logger.debug(
|
||||
f"Not HTML page: Accept header doesn't include text/html: {accept_header}"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.debug("IS HTML page request")
|
||||
@@ -379,13 +370,21 @@ def _redirect_to_login(request: Request) -> RedirectResponse:
|
||||
elif context_type == RequestContext.SHOP:
|
||||
# For shop context, redirect to shop login (customer login)
|
||||
# Calculate base_url for proper routing (supports domain, subdomain, and path-based access)
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
base_url = "/"
|
||||
if access_method == "path" and vendor:
|
||||
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/"
|
||||
)
|
||||
base_url = f"{full_prefix}{vendor.subdomain}/"
|
||||
|
||||
login_url = f"{base_url}shop/account/login"
|
||||
@@ -401,22 +400,28 @@ def _redirect_to_login(request: Request) -> RedirectResponse:
|
||||
def raise_not_found(resource_type: str, identifier: str) -> None:
|
||||
"""Convenience function to raise ResourceNotFoundException."""
|
||||
from .base import ResourceNotFoundException
|
||||
|
||||
raise ResourceNotFoundException(resource_type, identifier)
|
||||
|
||||
|
||||
def raise_validation_error(message: str, field: str = None, details: dict = None) -> None:
|
||||
def raise_validation_error(
|
||||
message: str, field: str = None, details: dict = None
|
||||
) -> None:
|
||||
"""Convenience function to raise ValidationException."""
|
||||
from .base import ValidationException
|
||||
|
||||
raise ValidationException(message, field, details)
|
||||
|
||||
|
||||
def raise_auth_error(message: str = "Authentication failed") -> None:
|
||||
"""Convenience function to raise AuthenticationException."""
|
||||
from .base import AuthenticationException
|
||||
|
||||
raise AuthenticationException(message)
|
||||
|
||||
|
||||
def raise_permission_error(message: str = "Access denied") -> None:
|
||||
"""Convenience function to raise AuthorizationException."""
|
||||
from .base import AuthorizationException
|
||||
|
||||
raise AuthorizationException(message)
|
||||
|
||||
@@ -4,7 +4,9 @@ Inventory management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import ResourceNotFoundException, ValidationException, BusinessLogicException
|
||||
|
||||
from .base import (BusinessLogicException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class InventoryNotFoundException(ResourceNotFoundException):
|
||||
@@ -14,7 +16,9 @@ class InventoryNotFoundException(ResourceNotFoundException):
|
||||
if identifier_type.lower() == "gtin":
|
||||
message = f"No inventory found for GTIN '{identifier}'"
|
||||
else:
|
||||
message = f"Inventory record with {identifier_type} '{identifier}' not found"
|
||||
message = (
|
||||
f"Inventory record with {identifier_type} '{identifier}' not found"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
resource_type="Inventory",
|
||||
@@ -28,11 +32,11 @@ class InsufficientInventoryException(BusinessLogicException):
|
||||
"""Raised when trying to remove more inventory than available."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gtin: str,
|
||||
location: str,
|
||||
requested: int,
|
||||
available: int,
|
||||
self,
|
||||
gtin: str,
|
||||
location: str,
|
||||
requested: int,
|
||||
available: int,
|
||||
):
|
||||
message = f"Insufficient inventory for GTIN '{gtin}' at '{location}'. Requested: {requested}, Available: {available}"
|
||||
|
||||
@@ -52,10 +56,10 @@ class InvalidInventoryOperationException(ValidationException):
|
||||
"""Raised when inventory operation is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
operation: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str,
|
||||
operation: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
if not details:
|
||||
details = {}
|
||||
@@ -74,10 +78,10 @@ class InventoryValidationException(ValidationException):
|
||||
"""Raised when inventory data validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Inventory validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
self,
|
||||
message: str = "Inventory validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
details = {}
|
||||
if validation_errors:
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Import/marketplace exceptions
|
||||
# Import/marketplace exceptions
|
||||
|
||||
@@ -4,24 +4,21 @@ Marketplace import specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
BusinessLogicException,
|
||||
AuthorizationException,
|
||||
ExternalServiceException
|
||||
)
|
||||
|
||||
from .base import (AuthorizationException, BusinessLogicException,
|
||||
ExternalServiceException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class MarketplaceImportException(BusinessLogicException):
|
||||
"""Base exception for marketplace import operations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
error_code: str = "MARKETPLACE_IMPORT_ERROR",
|
||||
marketplace: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str,
|
||||
error_code: str = "MARKETPLACE_IMPORT_ERROR",
|
||||
marketplace: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
if not details:
|
||||
details = {}
|
||||
@@ -67,11 +64,11 @@ class InvalidImportDataException(ValidationException):
|
||||
"""Raised when import data is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid import data",
|
||||
field: Optional[str] = None,
|
||||
row_number: Optional[int] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid import data",
|
||||
field: Optional[str] = None,
|
||||
row_number: Optional[int] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
if not details:
|
||||
details = {}
|
||||
@@ -118,7 +115,9 @@ class ImportJobCannotBeDeletedException(BusinessLogicException):
|
||||
class MarketplaceConnectionException(ExternalServiceException):
|
||||
"""Raised when marketplace connection fails."""
|
||||
|
||||
def __init__(self, marketplace: str, message: str = "Failed to connect to marketplace"):
|
||||
def __init__(
|
||||
self, marketplace: str, message: str = "Failed to connect to marketplace"
|
||||
):
|
||||
super().__init__(
|
||||
service=marketplace,
|
||||
message=f"{message}: {marketplace}",
|
||||
@@ -130,10 +129,10 @@ class MarketplaceDataParsingException(ValidationException):
|
||||
"""Raised when marketplace data cannot be parsed."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
marketplace: str,
|
||||
message: str = "Failed to parse marketplace data",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
marketplace: str,
|
||||
message: str = "Failed to parse marketplace data",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
if not details:
|
||||
details = {}
|
||||
@@ -150,10 +149,10 @@ class ImportRateLimitException(BusinessLogicException):
|
||||
"""Raised when import rate limit is exceeded."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_imports: int,
|
||||
time_window: str,
|
||||
retry_after: Optional[int] = None,
|
||||
self,
|
||||
max_imports: int,
|
||||
time_window: str,
|
||||
retry_after: Optional[int] = None,
|
||||
):
|
||||
details = {
|
||||
"max_imports": max_imports,
|
||||
|
||||
@@ -4,7 +4,9 @@ MarketplaceProduct management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import ResourceNotFoundException, ConflictException, ValidationException, BusinessLogicException
|
||||
|
||||
from .base import (BusinessLogicException, ConflictException,
|
||||
ResourceNotFoundException, ValidationException)
|
||||
|
||||
|
||||
class MarketplaceProductNotFoundException(ResourceNotFoundException):
|
||||
@@ -34,10 +36,10 @@ class InvalidMarketplaceProductDataException(ValidationException):
|
||||
"""Raised when product data is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid product data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid product data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -51,10 +53,10 @@ class MarketplaceProductValidationException(ValidationException):
|
||||
"""Raised when product validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
self,
|
||||
message: str,
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
details = {}
|
||||
if validation_errors:
|
||||
@@ -84,10 +86,10 @@ class MarketplaceProductCSVImportException(BusinessLogicException):
|
||||
"""Raised when product CSV import fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "MarketplaceProduct CSV import failed",
|
||||
row_number: Optional[int] = None,
|
||||
errors: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "MarketplaceProduct CSV import failed",
|
||||
row_number: Optional[int] = None,
|
||||
errors: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
details = {}
|
||||
if row_number:
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Media/file management exceptions
|
||||
# Media/file management exceptions
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Monitoring exceptions
|
||||
# Monitoring exceptions
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Notification exceptions
|
||||
# Notification exceptions
|
||||
|
||||
@@ -4,11 +4,9 @@ Order management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (BusinessLogicException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class OrderNotFoundException(ResourceNotFoundException):
|
||||
@@ -19,7 +17,7 @@ class OrderNotFoundException(ResourceNotFoundException):
|
||||
resource_type="Order",
|
||||
identifier=order_identifier,
|
||||
message=f"Order '{order_identifier}' not found",
|
||||
error_code="ORDER_NOT_FOUND"
|
||||
error_code="ORDER_NOT_FOUND",
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +28,7 @@ class OrderAlreadyExistsException(ValidationException):
|
||||
super().__init__(
|
||||
message=f"Order with number '{order_number}' already exists",
|
||||
error_code="ORDER_ALREADY_EXISTS",
|
||||
details={"order_number": order_number}
|
||||
details={"order_number": order_number},
|
||||
)
|
||||
|
||||
|
||||
@@ -39,9 +37,7 @@ class OrderValidationException(ValidationException):
|
||||
|
||||
def __init__(self, message: str, details: Optional[dict] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="ORDER_VALIDATION_FAILED",
|
||||
details=details
|
||||
message=message, error_code="ORDER_VALIDATION_FAILED", details=details
|
||||
)
|
||||
|
||||
|
||||
@@ -52,10 +48,7 @@ class InvalidOrderStatusException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Cannot change order status from '{current_status}' to '{new_status}'",
|
||||
error_code="INVALID_ORDER_STATUS_CHANGE",
|
||||
details={
|
||||
"current_status": current_status,
|
||||
"new_status": new_status
|
||||
}
|
||||
details={"current_status": current_status, "new_status": new_status},
|
||||
)
|
||||
|
||||
|
||||
@@ -66,8 +59,5 @@ class OrderCannotBeCancelledException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Order '{order_number}' cannot be cancelled: {reason}",
|
||||
error_code="ORDER_CANNOT_BE_CANCELLED",
|
||||
details={
|
||||
"order_number": order_number,
|
||||
"reason": reason
|
||||
}
|
||||
details={"order_number": order_number, "reason": reason},
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Payment processing exceptions
|
||||
# Payment processing exceptions
|
||||
|
||||
@@ -4,12 +4,9 @@ Product (vendor catalog) specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (BusinessLogicException, ConflictException,
|
||||
ResourceNotFoundException, ValidationException)
|
||||
|
||||
|
||||
class ProductNotFoundException(ResourceNotFoundException):
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Search exceptions
|
||||
# Search exceptions
|
||||
|
||||
@@ -4,13 +4,10 @@ Team management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
AuthorizationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (AuthorizationException, BusinessLogicException,
|
||||
ConflictException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class TeamMemberNotFoundException(ResourceNotFoundException):
|
||||
@@ -20,7 +17,9 @@ class TeamMemberNotFoundException(ResourceNotFoundException):
|
||||
details = {"user_id": user_id}
|
||||
if vendor_id:
|
||||
details["vendor_id"] = vendor_id
|
||||
message = f"Team member with user ID '{user_id}' not found in vendor {vendor_id}"
|
||||
message = (
|
||||
f"Team member with user ID '{user_id}' not found in vendor {vendor_id}"
|
||||
)
|
||||
else:
|
||||
message = f"Team member with user ID '{user_id}' not found"
|
||||
|
||||
@@ -84,7 +83,12 @@ class TeamInvitationAlreadyAcceptedException(ConflictException):
|
||||
class UnauthorizedTeamActionException(AuthorizationException):
|
||||
"""Raised when user tries to perform team action without permission."""
|
||||
|
||||
def __init__(self, action: str, user_id: Optional[int] = None, required_permission: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
action: str,
|
||||
user_id: Optional[int] = None,
|
||||
required_permission: Optional[str] = None,
|
||||
):
|
||||
details = {"action": action}
|
||||
if user_id:
|
||||
details["user_id"] = user_id
|
||||
@@ -147,10 +151,10 @@ class InvalidRoleException(ValidationException):
|
||||
"""Raised when role data is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid role data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid role data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -164,10 +168,10 @@ class InsufficientTeamPermissionsException(AuthorizationException):
|
||||
"""Raised when user lacks required team permissions for an action."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
required_permission: str,
|
||||
user_id: Optional[int] = None,
|
||||
action: Optional[str] = None,
|
||||
self,
|
||||
required_permission: str,
|
||||
user_id: Optional[int] = None,
|
||||
action: Optional[str] = None,
|
||||
):
|
||||
details = {"required_permission": required_permission}
|
||||
if user_id:
|
||||
@@ -202,10 +206,10 @@ class TeamValidationException(ValidationException):
|
||||
"""Raised when team operation validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Team operation validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
self,
|
||||
message: str = "Team operation validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
details = {}
|
||||
if validation_errors:
|
||||
@@ -223,10 +227,10 @@ class InvalidInvitationDataException(ValidationException):
|
||||
"""Raised when team invitation data is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid invitation data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid invitation data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -240,6 +244,7 @@ class InvalidInvitationDataException(ValidationException):
|
||||
# NEW: Add InvalidInvitationTokenException
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvalidInvitationTokenException(ValidationException):
|
||||
"""Raised when invitation token is invalid, expired, or already used.
|
||||
|
||||
@@ -248,9 +253,9 @@ class InvalidInvitationTokenException(ValidationException):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid or expired invitation token",
|
||||
invitation_token: Optional[str] = None
|
||||
self,
|
||||
message: str = "Invalid or expired invitation token",
|
||||
invitation_token: Optional[str] = None,
|
||||
):
|
||||
details = {}
|
||||
if invitation_token:
|
||||
|
||||
@@ -4,13 +4,10 @@ Vendor management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
AuthorizationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (AuthorizationException, BusinessLogicException,
|
||||
ConflictException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class VendorNotFoundException(ResourceNotFoundException):
|
||||
@@ -82,10 +79,10 @@ class InvalidVendorDataException(ValidationException):
|
||||
"""Raised when vendor data is invalid or incomplete."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid vendor data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid vendor data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -99,10 +96,10 @@ class VendorValidationException(ValidationException):
|
||||
"""Raised when vendor validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Vendor validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
self,
|
||||
message: str = "Vendor validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
details = {}
|
||||
if validation_errors:
|
||||
@@ -120,9 +117,9 @@ class IncompleteVendorDataException(ValidationException):
|
||||
"""Raised when vendor data is missing required fields."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vendor_code: str,
|
||||
missing_fields: list,
|
||||
self,
|
||||
vendor_code: str,
|
||||
missing_fields: list,
|
||||
):
|
||||
super().__init__(
|
||||
message=f"Vendor '{vendor_code}' is missing required fields: {', '.join(missing_fields)}",
|
||||
|
||||
@@ -4,13 +4,10 @@ Vendor domain management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
BusinessLogicException,
|
||||
ExternalServiceException
|
||||
)
|
||||
|
||||
from .base import (BusinessLogicException, ConflictException,
|
||||
ExternalServiceException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
|
||||
|
||||
class VendorDomainNotFoundException(ResourceNotFoundException):
|
||||
@@ -64,10 +61,7 @@ class ReservedDomainException(ValidationException):
|
||||
super().__init__(
|
||||
message=f"Domain cannot use reserved subdomain: {reserved_part}",
|
||||
field="domain",
|
||||
details={
|
||||
"domain": domain,
|
||||
"reserved_part": reserved_part
|
||||
},
|
||||
details={"domain": domain, "reserved_part": reserved_part},
|
||||
)
|
||||
self.error_code = "RESERVED_DOMAIN"
|
||||
|
||||
@@ -79,10 +73,7 @@ class DomainNotVerifiedException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Domain '{domain}' must be verified before activation",
|
||||
error_code="DOMAIN_NOT_VERIFIED",
|
||||
details={
|
||||
"domain_id": domain_id,
|
||||
"domain": domain
|
||||
},
|
||||
details={"domain_id": domain_id, "domain": domain},
|
||||
)
|
||||
|
||||
|
||||
@@ -93,10 +84,7 @@ class DomainVerificationFailedException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Domain verification failed for '{domain}': {reason}",
|
||||
error_code="DOMAIN_VERIFICATION_FAILED",
|
||||
details={
|
||||
"domain": domain,
|
||||
"reason": reason
|
||||
},
|
||||
details={"domain": domain, "reason": reason},
|
||||
)
|
||||
|
||||
|
||||
@@ -107,10 +95,7 @@ class DomainAlreadyVerifiedException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Domain '{domain}' is already verified",
|
||||
error_code="DOMAIN_ALREADY_VERIFIED",
|
||||
details={
|
||||
"domain_id": domain_id,
|
||||
"domain": domain
|
||||
},
|
||||
details={"domain_id": domain_id, "domain": domain},
|
||||
)
|
||||
|
||||
|
||||
@@ -133,10 +118,7 @@ class DNSVerificationException(ExternalServiceException):
|
||||
service_name="DNS",
|
||||
message=f"DNS verification failed for '{domain}': {reason}",
|
||||
error_code="DNS_VERIFICATION_ERROR",
|
||||
details={
|
||||
"domain": domain,
|
||||
"reason": reason
|
||||
},
|
||||
details={"domain": domain, "reason": reason},
|
||||
)
|
||||
|
||||
|
||||
@@ -147,10 +129,7 @@ class MaxDomainsReachedException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Maximum number of domains reached ({max_domains})",
|
||||
error_code="MAX_DOMAINS_REACHED",
|
||||
details={
|
||||
"vendor_id": vendor_id,
|
||||
"max_domains": max_domains
|
||||
},
|
||||
details={"vendor_id": vendor_id, "max_domains": max_domains},
|
||||
)
|
||||
|
||||
|
||||
@@ -161,8 +140,5 @@ class UnauthorizedDomainAccessException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Unauthorized access to domain {domain_id}",
|
||||
error_code="UNAUTHORIZED_DOMAIN_ACCESS",
|
||||
details={
|
||||
"domain_id": domain_id,
|
||||
"vendor_id": vendor_id
|
||||
},
|
||||
details={"domain_id": domain_id, "vendor_id": vendor_id},
|
||||
)
|
||||
|
||||
@@ -4,12 +4,9 @@ Vendor theme management specific exceptions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import (
|
||||
ResourceNotFoundException,
|
||||
ConflictException,
|
||||
ValidationException,
|
||||
BusinessLogicException
|
||||
)
|
||||
|
||||
from .base import (BusinessLogicException, ConflictException,
|
||||
ResourceNotFoundException, ValidationException)
|
||||
|
||||
|
||||
class VendorThemeNotFoundException(ResourceNotFoundException):
|
||||
@@ -28,10 +25,10 @@ class InvalidThemeDataException(ValidationException):
|
||||
"""Raised when theme data is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Invalid theme data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
self,
|
||||
message: str = "Invalid theme data",
|
||||
field: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -62,10 +59,10 @@ class ThemeValidationException(ValidationException):
|
||||
"""Raised when theme validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Theme validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
self,
|
||||
message: str = "Theme validation failed",
|
||||
field: Optional[str] = None,
|
||||
validation_errors: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
details = {}
|
||||
if validation_errors:
|
||||
@@ -86,10 +83,7 @@ class ThemePresetAlreadyAppliedException(BusinessLogicException):
|
||||
super().__init__(
|
||||
message=f"Preset '{preset_name}' is already applied to vendor '{vendor_code}'",
|
||||
error_code="THEME_PRESET_ALREADY_APPLIED",
|
||||
details={
|
||||
"preset_name": preset_name,
|
||||
"vendor_code": vendor_code
|
||||
},
|
||||
details={"preset_name": preset_name, "vendor_code": vendor_code},
|
||||
)
|
||||
|
||||
|
||||
@@ -120,18 +114,13 @@ class InvalidFontFamilyException(ValidationException):
|
||||
class ThemeOperationException(BusinessLogicException):
|
||||
"""Raised when theme operation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation: str,
|
||||
vendor_code: str,
|
||||
reason: str
|
||||
):
|
||||
def __init__(self, operation: str, vendor_code: str, reason: str):
|
||||
super().__init__(
|
||||
message=f"Theme operation '{operation}' failed for vendor '{vendor_code}': {reason}",
|
||||
error_code="THEME_OPERATION_FAILED",
|
||||
details={
|
||||
"operation": operation,
|
||||
"vendor_code": vendor_code,
|
||||
"reason": reason
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3,18 +3,23 @@ Architecture Scan Models
|
||||
Database models for tracking code quality scans and violations
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey, JSON
|
||||
from sqlalchemy import (JSON, Boolean, Column, DateTime, Float, ForeignKey,
|
||||
Integer, String, Text)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class ArchitectureScan(Base):
|
||||
"""Represents a single run of the architecture validator"""
|
||||
|
||||
__tablename__ = "architecture_scans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
timestamp = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
||||
)
|
||||
total_files = Column(Integer, default=0)
|
||||
total_violations = Column(Integer, default=0)
|
||||
errors = Column(Integer, default=0)
|
||||
@@ -24,7 +29,9 @@ class ArchitectureScan(Base):
|
||||
git_commit_hash = Column(String(40))
|
||||
|
||||
# Relationship to violations
|
||||
violations = relationship("ArchitectureViolation", back_populates="scan", cascade="all, delete-orphan")
|
||||
violations = relationship(
|
||||
"ArchitectureViolation", back_populates="scan", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureScan(id={self.id}, violations={self.total_violations}, errors={self.errors})>"
|
||||
@@ -32,31 +39,48 @@ class ArchitectureScan(Base):
|
||||
|
||||
class ArchitectureViolation(Base):
|
||||
"""Represents a single architectural violation found during a scan"""
|
||||
|
||||
__tablename__ = "architecture_violations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
scan_id = Column(Integer, ForeignKey("architecture_scans.id"), nullable=False, index=True)
|
||||
scan_id = Column(
|
||||
Integer, ForeignKey("architecture_scans.id"), nullable=False, index=True
|
||||
)
|
||||
rule_id = Column(String(20), nullable=False, index=True) # e.g., 'API-001'
|
||||
rule_name = Column(String(200), nullable=False)
|
||||
severity = Column(String(10), nullable=False, index=True) # 'error', 'warning', 'info'
|
||||
severity = Column(
|
||||
String(10), nullable=False, index=True
|
||||
) # 'error', 'warning', 'info'
|
||||
file_path = Column(String(500), nullable=False, index=True)
|
||||
line_number = Column(Integer, nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
context = Column(Text) # Code snippet
|
||||
suggestion = Column(Text)
|
||||
status = Column(String(20), default='open', index=True) # 'open', 'assigned', 'resolved', 'ignored', 'technical_debt'
|
||||
status = Column(
|
||||
String(20), default="open", index=True
|
||||
) # 'open', 'assigned', 'resolved', 'ignored', 'technical_debt'
|
||||
assigned_to = Column(Integer, ForeignKey("users.id"))
|
||||
resolved_at = Column(DateTime(timezone=True))
|
||||
resolved_by = Column(Integer, ForeignKey("users.id"))
|
||||
resolution_note = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
scan = relationship("ArchitectureScan", back_populates="violations")
|
||||
assigned_user = relationship("User", foreign_keys=[assigned_to], backref="assigned_violations")
|
||||
resolver = relationship("User", foreign_keys=[resolved_by], backref="resolved_violations")
|
||||
assignments = relationship("ViolationAssignment", back_populates="violation", cascade="all, delete-orphan")
|
||||
comments = relationship("ViolationComment", back_populates="violation", cascade="all, delete-orphan")
|
||||
assigned_user = relationship(
|
||||
"User", foreign_keys=[assigned_to], backref="assigned_violations"
|
||||
)
|
||||
resolver = relationship(
|
||||
"User", foreign_keys=[resolved_by], backref="resolved_violations"
|
||||
)
|
||||
assignments = relationship(
|
||||
"ViolationAssignment", back_populates="violation", cascade="all, delete-orphan"
|
||||
)
|
||||
comments = relationship(
|
||||
"ViolationComment", back_populates="violation", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureViolation(id={self.id}, rule={self.rule_id}, file={self.file_path}:{self.line_number})>"
|
||||
@@ -64,18 +88,30 @@ class ArchitectureViolation(Base):
|
||||
|
||||
class ArchitectureRule(Base):
|
||||
"""Architecture rules configuration (from YAML with database overrides)"""
|
||||
|
||||
__tablename__ = "architecture_rules"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
rule_id = Column(String(20), unique=True, nullable=False, index=True) # e.g., 'API-001'
|
||||
category = Column(String(50), nullable=False) # 'api_endpoint', 'service_layer', etc.
|
||||
rule_id = Column(
|
||||
String(20), unique=True, nullable=False, index=True
|
||||
) # e.g., 'API-001'
|
||||
category = Column(
|
||||
String(50), nullable=False
|
||||
) # 'api_endpoint', 'service_layer', etc.
|
||||
name = Column(String(200), nullable=False)
|
||||
description = Column(Text)
|
||||
severity = Column(String(10), nullable=False) # Can override default from YAML
|
||||
enabled = Column(Boolean, default=True, nullable=False)
|
||||
custom_config = Column(JSON) # For rule-specific settings
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ArchitectureRule(id={self.rule_id}, name={self.name}, enabled={self.enabled})>"
|
||||
@@ -83,20 +119,29 @@ class ArchitectureRule(Base):
|
||||
|
||||
class ViolationAssignment(Base):
|
||||
"""Tracks assignment of violations to developers"""
|
||||
|
||||
__tablename__ = "violation_assignments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
violation_id = Column(Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True)
|
||||
violation_id = Column(
|
||||
Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True
|
||||
)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
assigned_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
assigned_by = Column(Integer, ForeignKey("users.id"))
|
||||
due_date = Column(DateTime(timezone=True))
|
||||
priority = Column(String(10), default='medium') # 'low', 'medium', 'high', 'critical'
|
||||
priority = Column(
|
||||
String(10), default="medium"
|
||||
) # 'low', 'medium', 'high', 'critical'
|
||||
|
||||
# Relationships
|
||||
violation = relationship("ArchitectureViolation", back_populates="assignments")
|
||||
user = relationship("User", foreign_keys=[user_id], backref="violation_assignments")
|
||||
assigner = relationship("User", foreign_keys=[assigned_by], backref="assigned_by_me")
|
||||
assigner = relationship(
|
||||
"User", foreign_keys=[assigned_by], backref="assigned_by_me"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ViolationAssignment(id={self.id}, violation_id={self.violation_id}, user_id={self.user_id})>"
|
||||
@@ -104,13 +149,18 @@ class ViolationAssignment(Base):
|
||||
|
||||
class ViolationComment(Base):
|
||||
"""Comments on violations for collaboration"""
|
||||
|
||||
__tablename__ = "violation_comments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
violation_id = Column(Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True)
|
||||
violation_id = Column(
|
||||
Integer, ForeignKey("architecture_violations.id"), nullable=False, index=True
|
||||
)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
comment = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
violation = relationship("ArchitectureViolation", back_populates="comments")
|
||||
|
||||
@@ -30,17 +30,15 @@ Routes:
|
||||
- GET /code-quality/violations/{violation_id} → Violation details (auth required)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.api.deps import (
|
||||
get_current_admin_from_cookie_or_header,
|
||||
get_current_admin_optional,
|
||||
get_db
|
||||
)
|
||||
from app.api.deps import (get_current_admin_from_cookie_or_header,
|
||||
get_current_admin_optional, get_db)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
@@ -51,9 +49,10 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
# PUBLIC ROUTES (No Authentication Required)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def admin_root(
|
||||
current_user: Optional[User] = Depends(get_current_admin_optional)
|
||||
current_user: Optional[User] = Depends(get_current_admin_optional),
|
||||
):
|
||||
"""
|
||||
Redirect /admin/ based on authentication status.
|
||||
@@ -70,8 +69,7 @@ async def admin_root(
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_login_page(
|
||||
request: Request,
|
||||
current_user: Optional[User] = Depends(get_current_admin_optional)
|
||||
request: Request, current_user: Optional[User] = Depends(get_current_admin_optional)
|
||||
):
|
||||
"""
|
||||
Render admin login page.
|
||||
@@ -83,21 +81,19 @@ async def admin_login_page(
|
||||
# User is already logged in as admin, redirect to dashboard
|
||||
return RedirectResponse(url="/admin/dashboard", status_code=302)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin/login.html",
|
||||
{"request": request}
|
||||
)
|
||||
return templates.TemplateResponse("admin/login.html", {"request": request})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATED ROUTES (Admin Only)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_dashboard_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render admin dashboard page.
|
||||
@@ -108,7 +104,7 @@ async def admin_dashboard_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -116,11 +112,12 @@ async def admin_dashboard_page(
|
||||
# VENDOR MANAGEMENT ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_vendors_list_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendors management page.
|
||||
@@ -131,15 +128,15 @@ async def admin_vendors_list_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_vendor_create_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendor creation form.
|
||||
@@ -149,16 +146,18 @@ async def admin_vendor_create_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vendors/{vendor_code}", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/vendors/{vendor_code}", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def admin_vendor_detail_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendor detail page.
|
||||
@@ -170,16 +169,18 @@ async def admin_vendor_detail_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vendors/{vendor_code}/edit", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/vendors/{vendor_code}/edit", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def admin_vendor_edit_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendor edit form.
|
||||
@@ -190,7 +191,7 @@ async def admin_vendor_edit_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -198,12 +199,17 @@ async def admin_vendor_edit_page(
|
||||
# VENDOR DOMAINS ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/vendors/{vendor_code}/domains", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/vendors/{vendor_code}/domains",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def admin_vendor_domains_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendor domains management page.
|
||||
@@ -215,7 +221,7 @@ async def admin_vendor_domains_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -223,12 +229,15 @@ async def admin_vendor_domains_page(
|
||||
# VENDOR THEMES ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def admin_vendor_theme_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendor theme customization page.
|
||||
@@ -240,7 +249,7 @@ async def admin_vendor_theme_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -248,11 +257,12 @@ async def admin_vendor_theme_page(
|
||||
# USER MANAGEMENT ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/users", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_users_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render users management page.
|
||||
@@ -263,7 +273,7 @@ async def admin_users_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -271,11 +281,12 @@ async def admin_users_page(
|
||||
# IMPORT MANAGEMENT ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/imports", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_imports_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render imports management page.
|
||||
@@ -286,7 +297,7 @@ async def admin_imports_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -294,11 +305,12 @@ async def admin_imports_page(
|
||||
# SETTINGS ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/settings", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_settings_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render admin settings page.
|
||||
@@ -309,7 +321,7 @@ async def admin_settings_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -317,11 +329,12 @@ async def admin_settings_page(
|
||||
# CONTENT MANAGEMENT SYSTEM (CMS) ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/platform-homepage", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_platform_homepage_manager(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render platform homepage manager.
|
||||
@@ -332,15 +345,15 @@ async def admin_platform_homepage_manager(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_content_pages_list(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render content pages list.
|
||||
@@ -351,15 +364,17 @@ async def admin_content_pages_list(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/content-pages/create", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/content-pages/create", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def admin_content_page_create(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render create content page form.
|
||||
@@ -371,16 +386,20 @@ async def admin_content_page_create(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"page_id": None, # Indicates this is a create operation
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/content-pages/{page_id}/edit", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/content-pages/{page_id}/edit",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def admin_content_page_edit(
|
||||
request: Request,
|
||||
page_id: int = Path(..., description="Content page ID"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
page_id: int = Path(..., description="Content page ID"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render edit content page form.
|
||||
@@ -392,7 +411,7 @@ async def admin_content_page_edit(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"page_id": page_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -400,11 +419,12 @@ async def admin_content_page_edit(
|
||||
# DEVELOPER TOOLS - COMPONENTS & TESTING
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/components", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_components_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render UI components library page.
|
||||
@@ -415,7 +435,7 @@ async def admin_components_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -423,7 +443,7 @@ async def admin_components_page(
|
||||
async def admin_icons_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render icons browser page.
|
||||
@@ -434,15 +454,15 @@ async def admin_icons_page(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/testing", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_testing_hub(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render testing hub page.
|
||||
@@ -453,15 +473,15 @@ async def admin_testing_hub(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_test_auth_flow(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render authentication flow testing page.
|
||||
@@ -472,15 +492,19 @@ async def admin_test_auth_flow(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/test/vendors-users-migration", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/test/vendors-users-migration",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def admin_test_vendors_users_migration(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render vendors and users migration testing page.
|
||||
@@ -491,7 +515,7 @@ async def admin_test_vendors_users_migration(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -499,11 +523,12 @@ async def admin_test_vendors_users_migration(
|
||||
# CODE QUALITY & ARCHITECTURE ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_code_quality_dashboard(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render code quality dashboard.
|
||||
@@ -514,15 +539,17 @@ async def admin_code_quality_dashboard(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/code-quality/violations", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/code-quality/violations", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def admin_code_quality_violations(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render violations list page.
|
||||
@@ -533,16 +560,20 @@ async def admin_code_quality_violations(
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/code-quality/violations/{violation_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/code-quality/violations/{violation_id}",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def admin_code_quality_violation_detail(
|
||||
request: Request,
|
||||
violation_id: int = Path(..., description="Violation ID"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
violation_id: int = Path(..., description="Violation ID"),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render violation detail page.
|
||||
@@ -554,5 +585,5 @@ async def admin_code_quality_violation_detail(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"violation_id": violation_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -31,7 +31,8 @@ Routes (all mounted at /shop/* or /vendors/{code}/shop/* prefix):
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Request, Depends, Path
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -50,6 +51,7 @@ logger = logging.getLogger(__name__)
|
||||
# HELPER: Build Shop Template Context
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_shop_context(request: Request, db: Session = None, **extra_context) -> dict:
|
||||
"""
|
||||
Build template context for shop pages.
|
||||
@@ -76,13 +78,17 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
get_shop_context(request, db=db, user=current_user, product_id=123)
|
||||
"""
|
||||
# Extract from middleware state
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
theme = getattr(request.state, 'theme', None)
|
||||
clean_path = getattr(request.state, 'clean_path', request.url.path)
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
theme = getattr(request.state, "theme", None)
|
||||
clean_path = getattr(request.state, "clean_path", request.url.path)
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
|
||||
# Get detection method from vendor_context
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
if vendor is None:
|
||||
logger.warning(
|
||||
@@ -91,7 +97,7 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
"path": request.url.path,
|
||||
"host": request.headers.get("host", ""),
|
||||
"has_vendor": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Calculate base URL for links
|
||||
@@ -100,7 +106,11 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
base_url = "/"
|
||||
if access_method == "path" and vendor:
|
||||
# Use the full_prefix from vendor_context to determine which pattern was used
|
||||
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/"
|
||||
)
|
||||
base_url = f"{full_prefix}{vendor.subdomain}/"
|
||||
|
||||
# Load footer navigation pages from CMS if db session provided
|
||||
@@ -111,22 +121,16 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
vendor_id = vendor.id
|
||||
# Get pages configured to show in footer
|
||||
footer_pages = content_page_service.list_pages_for_vendor(
|
||||
db,
|
||||
vendor_id=vendor_id,
|
||||
footer_only=True,
|
||||
include_unpublished=False
|
||||
db, vendor_id=vendor_id, footer_only=True, include_unpublished=False
|
||||
)
|
||||
# Get pages configured to show in header
|
||||
header_pages = content_page_service.list_pages_for_vendor(
|
||||
db,
|
||||
vendor_id=vendor_id,
|
||||
header_only=True,
|
||||
include_unpublished=False
|
||||
db, vendor_id=vendor_id, header_only=True, include_unpublished=False
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[SHOP_CONTEXT] Failed to load navigation pages",
|
||||
extra={"error": str(e), "vendor_id": vendor.id if vendor else None}
|
||||
extra={"error": str(e), "vendor_id": vendor.id if vendor else None},
|
||||
)
|
||||
|
||||
context = {
|
||||
@@ -156,7 +160,7 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
"footer_pages_count": len(footer_pages),
|
||||
"header_pages_count": len(header_pages),
|
||||
"extra_keys": list(extra_context.keys()) if extra_context else [],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return context
|
||||
@@ -166,6 +170,7 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d
|
||||
# PUBLIC SHOP ROUTES (No Authentication Required)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_products_page(request: Request, db: Session = Depends(get_db)):
|
||||
@@ -177,21 +182,21 @@ async def shop_products_page(request: Request, db: Session = Depends(get_db)):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/products.html",
|
||||
get_shop_context(request, db=db)
|
||||
"shop/products.html", get_shop_context(request, db=db)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/products/{product_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/products/{product_id}", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def shop_product_detail_page(
|
||||
request: Request,
|
||||
product_id: int = Path(..., description="Product ID")
|
||||
request: Request, product_id: int = Path(..., description="Product ID")
|
||||
):
|
||||
"""
|
||||
Render product detail page.
|
||||
@@ -201,21 +206,21 @@ async def shop_product_detail_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/product.html",
|
||||
get_shop_context(request, product_id=product_id)
|
||||
"shop/product.html", get_shop_context(request, product_id=product_id)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def shop_category_page(
|
||||
request: Request,
|
||||
category_slug: str = Path(..., description="Category slug")
|
||||
request: Request, category_slug: str = Path(..., description="Category slug")
|
||||
):
|
||||
"""
|
||||
Render category products page.
|
||||
@@ -225,14 +230,13 @@ async def shop_category_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/category.html",
|
||||
get_shop_context(request, category_slug=category_slug)
|
||||
"shop/category.html", get_shop_context(request, category_slug=category_slug)
|
||||
)
|
||||
|
||||
|
||||
@@ -246,15 +250,12 @@ async def shop_cart_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/cart.html",
|
||||
get_shop_context(request)
|
||||
)
|
||||
return templates.TemplateResponse("shop/cart.html", get_shop_context(request))
|
||||
|
||||
|
||||
@router.get("/checkout", response_class=HTMLResponse, include_in_schema=False)
|
||||
@@ -267,15 +268,12 @@ async def shop_checkout_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/checkout.html",
|
||||
get_shop_context(request)
|
||||
)
|
||||
return templates.TemplateResponse("shop/checkout.html", get_shop_context(request))
|
||||
|
||||
|
||||
@router.get("/search", response_class=HTMLResponse, include_in_schema=False)
|
||||
@@ -288,21 +286,19 @@ async def shop_search_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/search.html",
|
||||
get_shop_context(request)
|
||||
)
|
||||
return templates.TemplateResponse("shop/search.html", get_shop_context(request))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CUSTOMER ACCOUNT - PUBLIC ROUTES (No Authentication)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/account/register", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_register_page(request: Request):
|
||||
"""
|
||||
@@ -313,14 +309,13 @@ async def shop_register_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/register.html",
|
||||
get_shop_context(request)
|
||||
"shop/account/register.html", get_shop_context(request)
|
||||
)
|
||||
|
||||
|
||||
@@ -334,18 +329,19 @@ async def shop_login_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/login.html",
|
||||
get_shop_context(request)
|
||||
"shop/account/login.html", get_shop_context(request)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/forgot-password", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/account/forgot-password", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def shop_forgot_password_page(request: Request):
|
||||
"""
|
||||
Render forgot password page.
|
||||
@@ -355,14 +351,13 @@ async def shop_forgot_password_page(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/forgot-password.html",
|
||||
get_shop_context(request)
|
||||
"shop/account/forgot-password.html", get_shop_context(request)
|
||||
)
|
||||
|
||||
|
||||
@@ -370,6 +365,7 @@ async def shop_forgot_password_page(request: Request):
|
||||
# CUSTOMER ACCOUNT - AUTHENTICATED ROUTES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/account", response_class=RedirectResponse, include_in_schema=False)
|
||||
@router.get("/account/", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def shop_account_root(request: Request):
|
||||
@@ -380,19 +376,27 @@ async def shop_account_root(request: Request):
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
# Get base_url from context for proper redirect
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
base_url = "/"
|
||||
if access_method == "path" and vendor:
|
||||
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/"
|
||||
)
|
||||
base_url = f"{full_prefix}{vendor.subdomain}/"
|
||||
|
||||
return RedirectResponse(url=f"{base_url}shop/account/dashboard", status_code=302)
|
||||
@@ -400,9 +404,9 @@ async def shop_account_root(request: Request):
|
||||
|
||||
@router.get("/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_account_dashboard_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer account dashboard.
|
||||
@@ -413,22 +417,21 @@ async def shop_account_dashboard_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/dashboard.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/dashboard.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/orders", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_orders_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer orders history page.
|
||||
@@ -439,23 +442,24 @@ async def shop_orders_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/orders.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/orders.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def shop_order_detail_page(
|
||||
request: Request,
|
||||
order_id: int = Path(..., description="Order ID"),
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
order_id: int = Path(..., description="Order ID"),
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer order detail page.
|
||||
@@ -466,22 +470,22 @@ async def shop_order_detail_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/order-detail.html",
|
||||
get_shop_context(request, user=current_customer, order_id=order_id)
|
||||
get_shop_context(request, user=current_customer, order_id=order_id),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/profile", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_profile_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer profile page.
|
||||
@@ -492,22 +496,21 @@ async def shop_profile_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/profile.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/profile.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/addresses", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_addresses_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer addresses management page.
|
||||
@@ -518,22 +521,21 @@ async def shop_addresses_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/addresses.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/addresses.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_wishlist_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer wishlist page.
|
||||
@@ -544,22 +546,21 @@ async def shop_wishlist_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/wishlist.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/wishlist.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/account/settings", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_settings_page(
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
request: Request,
|
||||
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render customer account settings page.
|
||||
@@ -570,14 +571,13 @@ async def shop_settings_page(
|
||||
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/account/settings.html",
|
||||
get_shop_context(request, user=current_customer)
|
||||
"shop/account/settings.html", get_shop_context(request, user=current_customer)
|
||||
)
|
||||
|
||||
|
||||
@@ -585,11 +585,12 @@ async def shop_settings_page(
|
||||
# DYNAMIC CONTENT PAGES (CMS)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def generic_content_page(
|
||||
request: Request,
|
||||
slug: str = Path(..., description="Content page slug"),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Generic content page handler (CMS).
|
||||
@@ -612,20 +613,17 @@ async def generic_content_page(
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"slug": slug,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_id = vendor.id if vendor else None
|
||||
|
||||
# Load content page from database (vendor override → platform default)
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=False
|
||||
db, slug=slug, vendor_id=vendor_id, include_unpublished=False
|
||||
)
|
||||
|
||||
if not page:
|
||||
@@ -635,7 +633,7 @@ async def generic_content_page(
|
||||
"slug": slug,
|
||||
"vendor_id": vendor_id,
|
||||
"vendor_name": vendor.name if vendor else None,
|
||||
}
|
||||
},
|
||||
)
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
@@ -647,12 +645,11 @@ async def generic_content_page(
|
||||
"page_title": page.title,
|
||||
"is_vendor_override": page.vendor_id is not None,
|
||||
"vendor_id": vendor_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/content-page.html",
|
||||
get_shop_context(request, page=page)
|
||||
"shop/content-page.html", get_shop_context(request, page=page)
|
||||
)
|
||||
|
||||
|
||||
@@ -660,6 +657,7 @@ async def generic_content_page(
|
||||
# DEBUG ENDPOINTS - For troubleshooting context issues
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/debug/context", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def debug_context(request: Request):
|
||||
"""
|
||||
@@ -670,8 +668,8 @@ async def debug_context(request: Request):
|
||||
|
||||
URL: /shop/debug/context
|
||||
"""
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
theme = getattr(request.state, 'theme', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
theme = getattr(request.state, "theme", None)
|
||||
|
||||
debug_info = {
|
||||
"path": request.url.path,
|
||||
@@ -687,12 +685,13 @@ async def debug_context(request: Request):
|
||||
"found": theme is not None,
|
||||
"name": theme.get("theme_name") if theme else None,
|
||||
},
|
||||
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
|
||||
"context_type": str(getattr(request.state, 'context_type', 'NOT SET')),
|
||||
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
|
||||
"context_type": str(getattr(request.state, "context_type", "NOT SET")),
|
||||
}
|
||||
|
||||
# Return as JSON-like HTML for easy reading
|
||||
import json
|
||||
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
@@ -21,18 +21,16 @@ Routes:
|
||||
- GET /vendor/{vendor_code}/settings → Vendor settings
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Path, HTTPException
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from app.api.deps import (
|
||||
get_current_vendor_from_cookie_or_header,
|
||||
get_current_vendor_optional,
|
||||
get_db
|
||||
)
|
||||
from app.api.deps import (get_current_vendor_from_cookie_or_header,
|
||||
get_current_vendor_optional, get_db)
|
||||
from app.services.content_page_service import content_page_service
|
||||
from models.database.user import User
|
||||
|
||||
@@ -46,6 +44,7 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
# PUBLIC ROUTES (No Authentication Required)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/{vendor_code}", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor code")):
|
||||
"""
|
||||
@@ -57,8 +56,8 @@ async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor
|
||||
|
||||
@router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def vendor_root(
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: Optional[User] = Depends(get_current_vendor_optional)
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: Optional[User] = Depends(get_current_vendor_optional),
|
||||
):
|
||||
"""
|
||||
Redirect /vendor/{code}/ based on authentication status.
|
||||
@@ -73,11 +72,13 @@ async def vendor_root(
|
||||
return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302)
|
||||
|
||||
|
||||
@router.get("/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_login_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: Optional[User] = Depends(get_current_vendor_optional)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: Optional[User] = Depends(get_current_vendor_optional),
|
||||
):
|
||||
"""
|
||||
Render vendor login page.
|
||||
@@ -99,7 +100,7 @@ async def vendor_login_page(
|
||||
{
|
||||
"request": request,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -107,11 +108,14 @@ async def vendor_login_page(
|
||||
# AUTHENTICATED ROUTES (Vendor Users Only)
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_dashboard_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render vendor dashboard.
|
||||
@@ -128,7 +132,7 @@ async def vendor_dashboard_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -136,11 +140,14 @@ async def vendor_dashboard_page(
|
||||
# PRODUCT MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_products_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render products management page.
|
||||
@@ -152,7 +159,7 @@ async def vendor_products_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -160,11 +167,14 @@ async def vendor_products_page(
|
||||
# ORDER MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_orders_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render orders management page.
|
||||
@@ -176,7 +186,7 @@ async def vendor_orders_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -184,11 +194,14 @@ async def vendor_orders_page(
|
||||
# CUSTOMER MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_customers_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render customers management page.
|
||||
@@ -200,7 +213,7 @@ async def vendor_customers_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -208,11 +221,14 @@ async def vendor_customers_page(
|
||||
# INVENTORY MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_inventory_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render inventory management page.
|
||||
@@ -224,7 +240,7 @@ async def vendor_inventory_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -232,11 +248,14 @@ async def vendor_inventory_page(
|
||||
# MARKETPLACE IMPORTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_marketplace_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render marketplace import page.
|
||||
@@ -248,7 +267,7 @@ async def vendor_marketplace_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -256,11 +275,12 @@ async def vendor_marketplace_page(
|
||||
# TEAM MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/{vendor_code}/team", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def vendor_team_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render team management page.
|
||||
@@ -272,7 +292,7 @@ async def vendor_team_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -280,11 +300,14 @@ async def vendor_team_page(
|
||||
# PROFILE & SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/profile", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/profile", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_profile_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render vendor profile page.
|
||||
@@ -296,15 +319,17 @@ async def vendor_profile_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get(
|
||||
"/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_settings_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Render vendor settings page.
|
||||
@@ -316,7 +341,7 @@ async def vendor_settings_page(
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -324,12 +349,15 @@ async def vendor_settings_page(
|
||||
# DYNAMIC CONTENT PAGES (CMS)
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/{vendor_code}/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
|
||||
@router.get(
|
||||
"/{vendor_code}/{slug}", response_class=HTMLResponse, include_in_schema=False
|
||||
)
|
||||
async def vendor_content_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
slug: str = Path(..., description="Content page slug"),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Generic content page handler for vendor shop (CMS).
|
||||
@@ -351,20 +379,17 @@ async def vendor_content_page(
|
||||
"path": request.url.path,
|
||||
"vendor_code": vendor_code,
|
||||
"slug": slug,
|
||||
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
|
||||
"context": getattr(request.state, 'context_type', 'NOT SET'),
|
||||
}
|
||||
"vendor": getattr(request.state, "vendor", "NOT SET"),
|
||||
"context": getattr(request.state, "context_type", "NOT SET"),
|
||||
},
|
||||
)
|
||||
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_id = vendor.id if vendor else None
|
||||
|
||||
# Load content page from database (vendor override → platform default)
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=False
|
||||
db, slug=slug, vendor_id=vendor_id, include_unpublished=False
|
||||
)
|
||||
|
||||
if not page:
|
||||
@@ -374,7 +399,7 @@ async def vendor_content_page(
|
||||
"slug": slug,
|
||||
"vendor_code": vendor_code,
|
||||
"vendor_id": vendor_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="Page not found")
|
||||
|
||||
@@ -385,7 +410,7 @@ async def vendor_content_page(
|
||||
"page_id": page.id,
|
||||
"is_vendor_override": page.vendor_id is not None,
|
||||
"vendor_id": vendor_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
@@ -394,5 +419,5 @@ async def vendor_content_page(
|
||||
"request": request,
|
||||
"page": page,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -10,15 +10,15 @@ This module provides functions for:
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Dict, Any
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import AdminOperationException
|
||||
from models.database.admin import AdminAuditLog
|
||||
from models.database.user import User
|
||||
from models.schema.admin import AdminAuditLogFilters, AdminAuditLogResponse
|
||||
from app.exceptions import AdminOperationException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +36,7 @@ class AdminAuditService:
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
request_id: Optional[str] = None
|
||||
request_id: Optional[str] = None,
|
||||
) -> AdminAuditLog:
|
||||
"""
|
||||
Log an admin action to the audit trail.
|
||||
@@ -63,7 +63,7 @@ class AdminAuditService:
|
||||
details=details or {},
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id
|
||||
request_id=request_id,
|
||||
)
|
||||
|
||||
db.add(audit_log)
|
||||
@@ -84,9 +84,7 @@ class AdminAuditService:
|
||||
return None
|
||||
|
||||
def get_audit_logs(
|
||||
self,
|
||||
db: Session,
|
||||
filters: AdminAuditLogFilters
|
||||
self, db: Session, filters: AdminAuditLogFilters
|
||||
) -> List[AdminAuditLogResponse]:
|
||||
"""
|
||||
Get filtered admin audit logs with pagination.
|
||||
@@ -98,7 +96,9 @@ class AdminAuditService:
|
||||
List of audit log responses
|
||||
"""
|
||||
try:
|
||||
query = db.query(AdminAuditLog).join(User, AdminAuditLog.admin_user_id == User.id)
|
||||
query = db.query(AdminAuditLog).join(
|
||||
User, AdminAuditLog.admin_user_id == User.id
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
conditions = []
|
||||
@@ -123,8 +123,7 @@ class AdminAuditService:
|
||||
|
||||
# Execute query with pagination
|
||||
logs = (
|
||||
query
|
||||
.order_by(AdminAuditLog.created_at.desc())
|
||||
query.order_by(AdminAuditLog.created_at.desc())
|
||||
.offset(filters.skip)
|
||||
.limit(filters.limit)
|
||||
.all()
|
||||
@@ -143,7 +142,7 @@ class AdminAuditService:
|
||||
ip_address=log.ip_address,
|
||||
user_agent=log.user_agent,
|
||||
request_id=log.request_id,
|
||||
created_at=log.created_at
|
||||
created_at=log.created_at,
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
@@ -151,15 +150,10 @@ class AdminAuditService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve audit logs: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_audit_logs",
|
||||
reason="Database query failed"
|
||||
operation="get_audit_logs", reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_audit_logs_count(
|
||||
self,
|
||||
db: Session,
|
||||
filters: AdminAuditLogFilters
|
||||
) -> int:
|
||||
def get_audit_logs_count(self, db: Session, filters: AdminAuditLogFilters) -> int:
|
||||
"""Get total count of audit logs matching filters."""
|
||||
try:
|
||||
query = db.query(AdminAuditLog)
|
||||
@@ -192,24 +186,14 @@ class AdminAuditService:
|
||||
return 0
|
||||
|
||||
def get_recent_actions_by_admin(
|
||||
self,
|
||||
db: Session,
|
||||
admin_user_id: int,
|
||||
limit: int = 10
|
||||
self, db: Session, admin_user_id: int, limit: int = 10
|
||||
) -> List[AdminAuditLogResponse]:
|
||||
"""Get recent actions by a specific admin."""
|
||||
filters = AdminAuditLogFilters(
|
||||
admin_user_id=admin_user_id,
|
||||
limit=limit
|
||||
)
|
||||
filters = AdminAuditLogFilters(admin_user_id=admin_user_id, limit=limit)
|
||||
return self.get_audit_logs(db, filters)
|
||||
|
||||
def get_actions_by_target(
|
||||
self,
|
||||
db: Session,
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
limit: int = 50
|
||||
self, db: Session, target_type: str, target_id: str, limit: int = 50
|
||||
) -> List[AdminAuditLogResponse]:
|
||||
"""Get all actions performed on a specific target."""
|
||||
try:
|
||||
@@ -218,7 +202,7 @@ class AdminAuditService:
|
||||
.filter(
|
||||
and_(
|
||||
AdminAuditLog.target_type == target_type,
|
||||
AdminAuditLog.target_id == str(target_id)
|
||||
AdminAuditLog.target_id == str(target_id),
|
||||
)
|
||||
)
|
||||
.order_by(AdminAuditLog.created_at.desc())
|
||||
@@ -236,7 +220,7 @@ class AdminAuditService:
|
||||
target_id=log.target_id,
|
||||
details=log.details,
|
||||
ip_address=log.ip_address,
|
||||
created_at=log.created_at
|
||||
created_at=log.created_at,
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
@@ -247,4 +231,4 @@ class AdminAuditService:
|
||||
|
||||
|
||||
# Create service instance
|
||||
admin_audit_service = AdminAuditService()
|
||||
admin_audit_service = AdminAuditService()
|
||||
|
||||
@@ -16,24 +16,19 @@ import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
UserNotFoundException,
|
||||
UserStatusChangeException,
|
||||
CannotModifySelfException,
|
||||
VendorNotFoundException,
|
||||
VendorAlreadyExistsException,
|
||||
VendorVerificationException,
|
||||
AdminOperationException,
|
||||
ValidationException,
|
||||
)
|
||||
from app.exceptions import (AdminOperationException, CannotModifySelfException,
|
||||
UserNotFoundException, UserStatusChangeException,
|
||||
ValidationException, VendorAlreadyExistsException,
|
||||
VendorNotFoundException,
|
||||
VendorVerificationException)
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Role, Vendor, VendorUser
|
||||
from models.schema.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.schema.vendor import VendorCreate
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.vendor import Vendor, Role, VendorUser
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,12 +47,11 @@ class AdminService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve users: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_all_users",
|
||||
reason="Database query failed"
|
||||
operation="get_all_users", reason="Database query failed"
|
||||
)
|
||||
|
||||
def toggle_user_status(
|
||||
self, db: Session, user_id: int, current_admin_id: int
|
||||
self, db: Session, user_id: int, current_admin_id: int
|
||||
) -> Tuple[User, str]:
|
||||
"""Toggle user active status."""
|
||||
user = self._get_user_by_id_or_raise(db, user_id)
|
||||
@@ -72,7 +66,7 @@ class AdminService:
|
||||
user_id=user_id,
|
||||
current_status="admin",
|
||||
attempted_action="toggle status",
|
||||
reason="Cannot modify another admin user"
|
||||
reason="Cannot modify another admin user",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -95,7 +89,7 @@ class AdminService:
|
||||
user_id=user_id,
|
||||
current_status="active" if original_status else "inactive",
|
||||
attempted_action="toggle status",
|
||||
reason="Database update failed"
|
||||
reason="Database update failed",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -103,7 +97,7 @@ class AdminService:
|
||||
# ============================================================================
|
||||
|
||||
def create_vendor_with_owner(
|
||||
self, db: Session, vendor_data: VendorCreate
|
||||
self, db: Session, vendor_data: VendorCreate
|
||||
) -> Tuple[Vendor, User, str]:
|
||||
"""
|
||||
Create vendor with owner user account.
|
||||
@@ -118,17 +112,23 @@ class AdminService:
|
||||
"""
|
||||
try:
|
||||
# Check if vendor code already exists
|
||||
existing_vendor = db.query(Vendor).filter(
|
||||
func.upper(Vendor.vendor_code) == vendor_data.vendor_code.upper()
|
||||
).first()
|
||||
existing_vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(
|
||||
func.upper(Vendor.vendor_code) == vendor_data.vendor_code.upper()
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_vendor:
|
||||
raise VendorAlreadyExistsException(vendor_data.vendor_code)
|
||||
|
||||
# Check if subdomain already exists
|
||||
existing_subdomain = db.query(Vendor).filter(
|
||||
func.lower(Vendor.subdomain) == vendor_data.subdomain.lower()
|
||||
).first()
|
||||
existing_subdomain = (
|
||||
db.query(Vendor)
|
||||
.filter(func.lower(Vendor.subdomain) == vendor_data.subdomain.lower())
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_subdomain:
|
||||
raise ValidationException(
|
||||
@@ -140,15 +140,14 @@ class AdminService:
|
||||
|
||||
# Create owner user with owner_email
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
auth_manager = AuthManager()
|
||||
|
||||
owner_username = f"{vendor_data.subdomain}_owner"
|
||||
owner_email = vendor_data.owner_email # ✅ For User authentication
|
||||
|
||||
# Check if user with this email already exists
|
||||
existing_user = db.query(User).filter(
|
||||
User.email == owner_email
|
||||
).first()
|
||||
existing_user = db.query(User).filter(User.email == owner_email).first()
|
||||
|
||||
if existing_user:
|
||||
# Use existing user as owner
|
||||
@@ -215,17 +214,17 @@ class AdminService:
|
||||
logger.error(f"Failed to create vendor: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="create_vendor_with_owner",
|
||||
reason=f"Failed to create vendor: {str(e)}"
|
||||
reason=f"Failed to create vendor: {str(e)}",
|
||||
)
|
||||
|
||||
def get_all_vendors(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_verified: Optional[bool] = None
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_verified: Optional[bool] = None,
|
||||
) -> Tuple[List[Vendor], int]:
|
||||
"""Get paginated list of all vendors with filtering."""
|
||||
try:
|
||||
@@ -238,7 +237,7 @@ class AdminService:
|
||||
or_(
|
||||
Vendor.name.ilike(search_term),
|
||||
Vendor.vendor_code.ilike(search_term),
|
||||
Vendor.subdomain.ilike(search_term)
|
||||
Vendor.subdomain.ilike(search_term),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -255,8 +254,7 @@ class AdminService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve vendors: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_all_vendors",
|
||||
reason="Database query failed"
|
||||
operation="get_all_vendors", reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_vendor_by_id(self, db: Session, vendor_id: int) -> Vendor:
|
||||
@@ -290,7 +288,7 @@ class AdminService:
|
||||
raise VendorVerificationException(
|
||||
vendor_id=vendor_id,
|
||||
reason="Database update failed",
|
||||
current_verification_status=original_status
|
||||
current_verification_status=original_status,
|
||||
)
|
||||
|
||||
def toggle_vendor_status(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
|
||||
@@ -317,7 +315,7 @@ class AdminService:
|
||||
operation="toggle_vendor_status",
|
||||
reason="Database update failed",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id)
|
||||
target_id=str(vendor_id),
|
||||
)
|
||||
|
||||
def delete_vendor(self, db: Session, vendor_id: int) -> str:
|
||||
@@ -345,15 +343,11 @@ class AdminService:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to delete vendor {vendor_id}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="delete_vendor",
|
||||
reason="Database deletion failed"
|
||||
operation="delete_vendor", reason="Database deletion failed"
|
||||
)
|
||||
|
||||
def update_vendor(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
vendor_update # VendorUpdate schema
|
||||
self, db: Session, vendor_id: int, vendor_update # VendorUpdate schema
|
||||
) -> Vendor:
|
||||
"""
|
||||
Update vendor information (Admin only).
|
||||
@@ -387,11 +381,18 @@ class AdminService:
|
||||
update_data = vendor_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Check subdomain uniqueness if changing
|
||||
if 'subdomain' in update_data and update_data['subdomain'] != vendor.subdomain:
|
||||
existing = db.query(Vendor).filter(
|
||||
Vendor.subdomain == update_data['subdomain'],
|
||||
Vendor.id != vendor_id
|
||||
).first()
|
||||
if (
|
||||
"subdomain" in update_data
|
||||
and update_data["subdomain"] != vendor.subdomain
|
||||
):
|
||||
existing = (
|
||||
db.query(Vendor)
|
||||
.filter(
|
||||
Vendor.subdomain == update_data["subdomain"],
|
||||
Vendor.id != vendor_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise ValidationException(
|
||||
f"Subdomain '{update_data['subdomain']}' is already taken"
|
||||
@@ -419,17 +420,16 @@ class AdminService:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to update vendor {vendor_id}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="update_vendor",
|
||||
reason=f"Database update failed: {str(e)}"
|
||||
operation="update_vendor", reason=f"Database update failed: {str(e)}"
|
||||
)
|
||||
|
||||
# Add this NEW method for transferring ownership:
|
||||
|
||||
def transfer_vendor_ownership(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
transfer_data # VendorTransferOwnership schema
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
transfer_data, # VendorTransferOwnership schema
|
||||
) -> Tuple[Vendor, User, User]:
|
||||
"""
|
||||
Transfer vendor ownership to another user.
|
||||
@@ -466,9 +466,9 @@ class AdminService:
|
||||
old_owner = vendor.owner
|
||||
|
||||
# Get new owner
|
||||
new_owner = db.query(User).filter(
|
||||
User.id == transfer_data.new_owner_user_id
|
||||
).first()
|
||||
new_owner = (
|
||||
db.query(User).filter(User.id == transfer_data.new_owner_user_id).first()
|
||||
)
|
||||
|
||||
if not new_owner:
|
||||
raise UserNotFoundException(str(transfer_data.new_owner_user_id))
|
||||
@@ -487,26 +487,32 @@ class AdminService:
|
||||
|
||||
try:
|
||||
# Get Owner role for this vendor
|
||||
owner_role = db.query(Role).filter(
|
||||
Role.vendor_id == vendor_id,
|
||||
Role.name == "Owner"
|
||||
).first()
|
||||
owner_role = (
|
||||
db.query(Role)
|
||||
.filter(Role.vendor_id == vendor_id, Role.name == "Owner")
|
||||
.first()
|
||||
)
|
||||
|
||||
if not owner_role:
|
||||
raise ValidationException("Owner role not found for vendor")
|
||||
|
||||
# Get Manager role (to demote old owner)
|
||||
manager_role = db.query(Role).filter(
|
||||
Role.vendor_id == vendor_id,
|
||||
Role.name == "Manager"
|
||||
).first()
|
||||
manager_role = (
|
||||
db.query(Role)
|
||||
.filter(Role.vendor_id == vendor_id, Role.name == "Manager")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Remove old owner from Owner role
|
||||
old_owner_link = db.query(VendorUser).filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == old_owner.id,
|
||||
VendorUser.role_id == owner_role.id
|
||||
).first()
|
||||
old_owner_link = (
|
||||
db.query(VendorUser)
|
||||
.filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == old_owner.id,
|
||||
VendorUser.role_id == owner_role.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if old_owner_link:
|
||||
if manager_role:
|
||||
@@ -525,10 +531,14 @@ class AdminService:
|
||||
)
|
||||
|
||||
# Check if new owner already has a vendor_user link
|
||||
new_owner_link = db.query(VendorUser).filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == new_owner.id
|
||||
).first()
|
||||
new_owner_link = (
|
||||
db.query(VendorUser)
|
||||
.filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == new_owner.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if new_owner_link:
|
||||
# Update existing link to Owner role
|
||||
@@ -540,7 +550,7 @@ class AdminService:
|
||||
vendor_id=vendor_id,
|
||||
user_id=new_owner.id,
|
||||
role_id=owner_role.id,
|
||||
is_active=True
|
||||
is_active=True,
|
||||
)
|
||||
db.add(new_owner_link)
|
||||
|
||||
@@ -568,10 +578,12 @@ class AdminService:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to transfer ownership for vendor {vendor_id}: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to transfer ownership for vendor {vendor_id}: {str(e)}"
|
||||
)
|
||||
raise AdminOperationException(
|
||||
operation="transfer_vendor_ownership",
|
||||
reason=f"Ownership transfer failed: {str(e)}"
|
||||
reason=f"Ownership transfer failed: {str(e)}",
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -579,13 +591,13 @@ class AdminService:
|
||||
# ============================================================================
|
||||
|
||||
def get_marketplace_import_jobs(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[MarketplaceImportJobResponse]:
|
||||
"""Get filtered and paginated marketplace import jobs."""
|
||||
try:
|
||||
@@ -596,7 +608,9 @@ class AdminService:
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%")
|
||||
)
|
||||
if status:
|
||||
query = query.filter(MarketplaceImportJob.status == status)
|
||||
|
||||
@@ -612,8 +626,7 @@ class AdminService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve marketplace import jobs: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_marketplace_import_jobs",
|
||||
reason="Database query failed"
|
||||
operation="get_marketplace_import_jobs", reason="Database query failed"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -624,10 +637,7 @@ class AdminService:
|
||||
"""Get recently created vendors."""
|
||||
try:
|
||||
vendors = (
|
||||
db.query(Vendor)
|
||||
.order_by(Vendor.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
db.query(Vendor).order_by(Vendor.created_at.desc()).limit(limit).all()
|
||||
)
|
||||
|
||||
return [
|
||||
@@ -638,7 +648,7 @@ class AdminService:
|
||||
"subdomain": v.subdomain,
|
||||
"is_active": v.is_active,
|
||||
"is_verified": v.is_verified,
|
||||
"created_at": v.created_at
|
||||
"created_at": v.created_at,
|
||||
}
|
||||
for v in vendors
|
||||
]
|
||||
@@ -663,7 +673,7 @@ class AdminService:
|
||||
"vendor_name": j.vendor_name,
|
||||
"status": j.status,
|
||||
"total_processed": j.total_processed or 0,
|
||||
"created_at": j.created_at
|
||||
"created_at": j.created_at,
|
||||
}
|
||||
for j in jobs
|
||||
]
|
||||
@@ -692,47 +702,53 @@ class AdminService:
|
||||
def _generate_temp_password(self, length: int = 12) -> str:
|
||||
"""Generate secure temporary password."""
|
||||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def _create_default_roles(self, db: Session, vendor_id: int):
|
||||
"""Create default roles for a new vendor."""
|
||||
default_roles = [
|
||||
{
|
||||
"name": "Owner",
|
||||
"permissions": ["*"] # Full access
|
||||
},
|
||||
{"name": "Owner", "permissions": ["*"]}, # Full access
|
||||
{
|
||||
"name": "Manager",
|
||||
"permissions": [
|
||||
"products.*", "orders.*", "customers.view",
|
||||
"inventory.*", "team.view"
|
||||
]
|
||||
"products.*",
|
||||
"orders.*",
|
||||
"customers.view",
|
||||
"inventory.*",
|
||||
"team.view",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Editor",
|
||||
"permissions": [
|
||||
"products.view", "products.edit",
|
||||
"orders.view", "inventory.view"
|
||||
]
|
||||
"products.view",
|
||||
"products.edit",
|
||||
"orders.view",
|
||||
"inventory.view",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Viewer",
|
||||
"permissions": [
|
||||
"products.view", "orders.view",
|
||||
"customers.view", "inventory.view"
|
||||
]
|
||||
}
|
||||
"products.view",
|
||||
"orders.view",
|
||||
"customers.view",
|
||||
"inventory.view",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for role_data in default_roles:
|
||||
role = Role(
|
||||
vendor_id=vendor_id,
|
||||
name=role_data["name"],
|
||||
permissions=role_data["permissions"]
|
||||
permissions=role_data["permissions"],
|
||||
)
|
||||
db.add(role)
|
||||
|
||||
def _convert_job_to_response(self, job: MarketplaceImportJob) -> MarketplaceImportJobResponse:
|
||||
def _convert_job_to_response(
|
||||
self, job: MarketplaceImportJob
|
||||
) -> MarketplaceImportJobResponse:
|
||||
"""Convert database model to response schema."""
|
||||
return MarketplaceImportJobResponse(
|
||||
job_id=job.id,
|
||||
|
||||
@@ -8,25 +8,19 @@ This module provides functions for:
|
||||
- Encrypting sensitive settings
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Optional, List, Any, Dict
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (AdminOperationException, ResourceNotFoundException,
|
||||
ValidationException)
|
||||
from models.database.admin import AdminSetting
|
||||
from models.schema.admin import (
|
||||
AdminSettingCreate,
|
||||
AdminSettingResponse,
|
||||
AdminSettingUpdate
|
||||
)
|
||||
from app.exceptions import (
|
||||
AdminOperationException,
|
||||
ValidationException,
|
||||
ResourceNotFoundException
|
||||
)
|
||||
from models.schema.admin import (AdminSettingCreate, AdminSettingResponse,
|
||||
AdminSettingUpdate)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,26 +28,19 @@ logger = logging.getLogger(__name__)
|
||||
class AdminSettingsService:
|
||||
"""Service for managing platform-wide settings."""
|
||||
|
||||
def get_setting_by_key(
|
||||
self,
|
||||
db: Session,
|
||||
key: str
|
||||
) -> Optional[AdminSetting]:
|
||||
def get_setting_by_key(self, db: Session, key: str) -> Optional[AdminSetting]:
|
||||
"""Get setting by key."""
|
||||
try:
|
||||
return db.query(AdminSetting).filter(
|
||||
func.lower(AdminSetting.key) == key.lower()
|
||||
).first()
|
||||
return (
|
||||
db.query(AdminSetting)
|
||||
.filter(func.lower(AdminSetting.key) == key.lower())
|
||||
.first()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get setting {key}: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_setting_value(
|
||||
self,
|
||||
db: Session,
|
||||
key: str,
|
||||
default: Any = None
|
||||
) -> Any:
|
||||
def get_setting_value(self, db: Session, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get setting value with type conversion.
|
||||
|
||||
@@ -76,7 +63,7 @@ class AdminSettingsService:
|
||||
elif setting.value_type == "float":
|
||||
return float(setting.value)
|
||||
elif setting.value_type == "boolean":
|
||||
return setting.value.lower() in ('true', '1', 'yes')
|
||||
return setting.value.lower() in ("true", "1", "yes")
|
||||
elif setting.value_type == "json":
|
||||
return json.loads(setting.value)
|
||||
else:
|
||||
@@ -86,10 +73,10 @@ class AdminSettingsService:
|
||||
return default
|
||||
|
||||
def get_all_settings(
|
||||
self,
|
||||
db: Session,
|
||||
category: Optional[str] = None,
|
||||
is_public: Optional[bool] = None
|
||||
self,
|
||||
db: Session,
|
||||
category: Optional[str] = None,
|
||||
is_public: Optional[bool] = None,
|
||||
) -> List[AdminSettingResponse]:
|
||||
"""Get all settings with optional filtering."""
|
||||
try:
|
||||
@@ -104,22 +91,16 @@ class AdminSettingsService:
|
||||
settings = query.order_by(AdminSetting.category, AdminSetting.key).all()
|
||||
|
||||
return [
|
||||
AdminSettingResponse.model_validate(setting)
|
||||
for setting in settings
|
||||
AdminSettingResponse.model_validate(setting) for setting in settings
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get settings: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_all_settings",
|
||||
reason="Database query failed"
|
||||
operation="get_all_settings", reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_settings_by_category(
|
||||
self,
|
||||
db: Session,
|
||||
category: str
|
||||
) -> Dict[str, Any]:
|
||||
def get_settings_by_category(self, db: Session, category: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get all settings in a category as a dictionary.
|
||||
|
||||
@@ -136,7 +117,7 @@ class AdminSettingsService:
|
||||
elif setting.value_type == "float":
|
||||
result[setting.key] = float(setting.value)
|
||||
elif setting.value_type == "boolean":
|
||||
result[setting.key] = setting.value.lower() in ('true', '1', 'yes')
|
||||
result[setting.key] = setting.value.lower() in ("true", "1", "yes")
|
||||
elif setting.value_type == "json":
|
||||
result[setting.key] = json.loads(setting.value)
|
||||
else:
|
||||
@@ -145,10 +126,7 @@ class AdminSettingsService:
|
||||
return result
|
||||
|
||||
def create_setting(
|
||||
self,
|
||||
db: Session,
|
||||
setting_data: AdminSettingCreate,
|
||||
admin_user_id: int
|
||||
self, db: Session, setting_data: AdminSettingCreate, admin_user_id: int
|
||||
) -> AdminSettingResponse:
|
||||
"""Create new setting."""
|
||||
try:
|
||||
@@ -176,7 +154,7 @@ class AdminSettingsService:
|
||||
description=setting_data.description,
|
||||
is_encrypted=setting_data.is_encrypted,
|
||||
is_public=setting_data.is_public,
|
||||
last_modified_by_user_id=admin_user_id
|
||||
last_modified_by_user_id=admin_user_id,
|
||||
)
|
||||
|
||||
db.add(setting)
|
||||
@@ -194,25 +172,17 @@ class AdminSettingsService:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to create setting: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="create_setting",
|
||||
reason="Database operation failed"
|
||||
operation="create_setting", reason="Database operation failed"
|
||||
)
|
||||
|
||||
def update_setting(
|
||||
self,
|
||||
db: Session,
|
||||
key: str,
|
||||
update_data: AdminSettingUpdate,
|
||||
admin_user_id: int
|
||||
self, db: Session, key: str, update_data: AdminSettingUpdate, admin_user_id: int
|
||||
) -> AdminSettingResponse:
|
||||
"""Update existing setting."""
|
||||
setting = self.get_setting_by_key(db, key)
|
||||
|
||||
if not setting:
|
||||
raise ResourceNotFoundException(
|
||||
resource_type="setting",
|
||||
identifier=key
|
||||
)
|
||||
raise ResourceNotFoundException(resource_type="setting", identifier=key)
|
||||
|
||||
try:
|
||||
# Validate new value
|
||||
@@ -244,42 +214,29 @@ class AdminSettingsService:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to update setting {key}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="update_setting",
|
||||
reason="Database operation failed"
|
||||
operation="update_setting", reason="Database operation failed"
|
||||
)
|
||||
|
||||
def upsert_setting(
|
||||
self,
|
||||
db: Session,
|
||||
setting_data: AdminSettingCreate,
|
||||
admin_user_id: int
|
||||
self, db: Session, setting_data: AdminSettingCreate, admin_user_id: int
|
||||
) -> AdminSettingResponse:
|
||||
"""Create or update setting (upsert)."""
|
||||
existing = self.get_setting_by_key(db, setting_data.key)
|
||||
|
||||
if existing:
|
||||
update_data = AdminSettingUpdate(
|
||||
value=setting_data.value,
|
||||
description=setting_data.description
|
||||
value=setting_data.value, description=setting_data.description
|
||||
)
|
||||
return self.update_setting(db, setting_data.key, update_data, admin_user_id)
|
||||
else:
|
||||
return self.create_setting(db, setting_data, admin_user_id)
|
||||
|
||||
def delete_setting(
|
||||
self,
|
||||
db: Session,
|
||||
key: str,
|
||||
admin_user_id: int
|
||||
) -> str:
|
||||
def delete_setting(self, db: Session, key: str, admin_user_id: int) -> str:
|
||||
"""Delete setting."""
|
||||
setting = self.get_setting_by_key(db, key)
|
||||
|
||||
if not setting:
|
||||
raise ResourceNotFoundException(
|
||||
resource_type="setting",
|
||||
identifier=key
|
||||
)
|
||||
raise ResourceNotFoundException(resource_type="setting", identifier=key)
|
||||
|
||||
try:
|
||||
db.delete(setting)
|
||||
@@ -293,8 +250,7 @@ class AdminSettingsService:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to delete setting {key}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="delete_setting",
|
||||
reason="Database operation failed"
|
||||
operation="delete_setting", reason="Database operation failed"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -309,7 +265,7 @@ class AdminSettingsService:
|
||||
elif value_type == "float":
|
||||
float(value)
|
||||
elif value_type == "boolean":
|
||||
if value.lower() not in ('true', 'false', '1', '0', 'yes', 'no'):
|
||||
if value.lower() not in ("true", "false", "1", "0", "yes", "no"):
|
||||
raise ValueError("Invalid boolean value")
|
||||
elif value_type == "json":
|
||||
json.loads(value)
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Audit logging services
|
||||
# Audit logging services
|
||||
|
||||
@@ -13,15 +13,12 @@ from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
UserAlreadyExistsException,
|
||||
InvalidCredentialsException,
|
||||
UserNotActiveException,
|
||||
ValidationException,
|
||||
)
|
||||
from app.exceptions import (InvalidCredentialsException,
|
||||
UserAlreadyExistsException, UserNotActiveException,
|
||||
ValidationException)
|
||||
from middleware.auth import AuthManager
|
||||
from models.schema.auth import UserLogin, UserRegister
|
||||
from models.database.user import User
|
||||
from models.schema.auth import UserLogin, UserRegister
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,11 +48,15 @@ class AuthService:
|
||||
try:
|
||||
# Check if email already exists
|
||||
if self._email_exists(db, user_data.email):
|
||||
raise UserAlreadyExistsException("Email already registered", field="email")
|
||||
raise UserAlreadyExistsException(
|
||||
"Email already registered", field="email"
|
||||
)
|
||||
|
||||
# Check if username already exists
|
||||
if self._username_exists(db, user_data.username):
|
||||
raise UserAlreadyExistsException("Username already taken", field="username")
|
||||
raise UserAlreadyExistsException(
|
||||
"Username already taken", field="username"
|
||||
)
|
||||
|
||||
# Hash password and create user
|
||||
hashed_password = self.auth_manager.hash_password(user_data.password)
|
||||
@@ -182,7 +183,9 @@ class AuthService:
|
||||
Dictionary with access_token, token_type, and expires_in
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from jose import jwt
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
try:
|
||||
@@ -217,6 +220,5 @@ class AuthService:
|
||||
return db.query(User).filter(User.username == username).first() is not None
|
||||
|
||||
|
||||
|
||||
# Create service instance following the same pattern as other services
|
||||
auth_service = AuthService()
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Backup and recovery services
|
||||
# Backup and recovery services
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Caching services
|
||||
# Caching services
|
||||
|
||||
@@ -9,23 +9,20 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (CartItemNotFoundException, CartValidationException,
|
||||
InsufficientInventoryForCartException,
|
||||
InvalidCartQuantityException,
|
||||
ProductNotAvailableForCartException,
|
||||
ProductNotFoundException)
|
||||
from models.database.cart import CartItem
|
||||
from models.database.product import Product
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.cart import CartItem
|
||||
from app.exceptions import (
|
||||
ProductNotFoundException,
|
||||
CartItemNotFoundException,
|
||||
CartValidationException,
|
||||
InsufficientInventoryForCartException,
|
||||
InvalidCartQuantityException,
|
||||
ProductNotAvailableForCartException,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,12 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
class CartService:
|
||||
"""Service for managing shopping carts."""
|
||||
|
||||
def get_cart(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str
|
||||
) -> Dict:
|
||||
def get_cart(self, db: Session, vendor_id: int, session_id: str) -> Dict:
|
||||
"""
|
||||
Get cart contents for a session.
|
||||
|
||||
@@ -55,20 +47,21 @@ class CartService:
|
||||
extra={
|
||||
"vendor_id": vendor_id,
|
||||
"session_id": session_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Fetch cart items from database
|
||||
cart_items = db.query(CartItem).filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id
|
||||
cart_items = (
|
||||
db.query(CartItem)
|
||||
.filter(
|
||||
and_(CartItem.vendor_id == vendor_id, CartItem.session_id == session_id)
|
||||
)
|
||||
).all()
|
||||
.all()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[CART_SERVICE] Found {len(cart_items)} items in database",
|
||||
extra={"item_count": len(cart_items)}
|
||||
extra={"item_count": len(cart_items)},
|
||||
)
|
||||
|
||||
# Build response
|
||||
@@ -79,14 +72,20 @@ class CartService:
|
||||
product = cart_item.product
|
||||
line_total = cart_item.line_total
|
||||
|
||||
items.append({
|
||||
"product_id": product.id,
|
||||
"product_name": product.marketplace_product.title,
|
||||
"quantity": cart_item.quantity,
|
||||
"price": cart_item.price_at_add,
|
||||
"line_total": line_total,
|
||||
"image_url": product.marketplace_product.image_link if product.marketplace_product else None,
|
||||
})
|
||||
items.append(
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_name": product.marketplace_product.title,
|
||||
"quantity": cart_item.quantity,
|
||||
"price": cart_item.price_at_add,
|
||||
"line_total": line_total,
|
||||
"image_url": (
|
||||
product.marketplace_product.image_link
|
||||
if product.marketplace_product
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
subtotal += line_total
|
||||
|
||||
@@ -95,23 +94,23 @@ class CartService:
|
||||
"session_id": session_id,
|
||||
"items": items,
|
||||
"subtotal": subtotal,
|
||||
"total": subtotal # Could add tax/shipping later
|
||||
"total": subtotal, # Could add tax/shipping later
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"[CART_SERVICE] get_cart returning: {len(cart_data['items'])} items, total: {cart_data['total']}",
|
||||
extra={"cart": cart_data}
|
||||
extra={"cart": cart_data},
|
||||
)
|
||||
|
||||
return cart_data
|
||||
|
||||
def add_to_cart(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
product_id: int,
|
||||
quantity: int = 1
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
product_id: int,
|
||||
quantity: int = 1,
|
||||
) -> Dict:
|
||||
"""
|
||||
Add product to cart.
|
||||
@@ -136,23 +135,27 @@ class CartService:
|
||||
"vendor_id": vendor_id,
|
||||
"session_id": session_id,
|
||||
"product_id": product_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
"quantity": quantity,
|
||||
},
|
||||
)
|
||||
|
||||
# Verify product exists and belongs to vendor
|
||||
product = db.query(Product).filter(
|
||||
and_(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
and_(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not product:
|
||||
logger.error(
|
||||
f"[CART_SERVICE] Product not found",
|
||||
extra={"product_id": product_id, "vendor_id": vendor_id}
|
||||
extra={"product_id": product_id, "vendor_id": vendor_id},
|
||||
)
|
||||
raise ProductNotFoundException(product_id=product_id, vendor_id=vendor_id)
|
||||
|
||||
@@ -161,21 +164,25 @@ class CartService:
|
||||
extra={
|
||||
"product_id": product_id,
|
||||
"product_name": product.marketplace_product.title,
|
||||
"available_inventory": product.available_inventory
|
||||
}
|
||||
"available_inventory": product.available_inventory,
|
||||
},
|
||||
)
|
||||
|
||||
# Get current price (use sale_price if available, otherwise regular price)
|
||||
current_price = product.sale_price if product.sale_price else product.price
|
||||
|
||||
# Check if item already exists in cart
|
||||
existing_item = db.query(CartItem).filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id
|
||||
existing_item = (
|
||||
db.query(CartItem)
|
||||
.filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_item:
|
||||
# Update quantity
|
||||
@@ -190,14 +197,14 @@ class CartService:
|
||||
"current_in_cart": existing_item.quantity,
|
||||
"adding": quantity,
|
||||
"requested_total": new_quantity,
|
||||
"available": product.available_inventory
|
||||
}
|
||||
"available": product.available_inventory,
|
||||
},
|
||||
)
|
||||
raise InsufficientInventoryForCartException(
|
||||
product_id=product_id,
|
||||
product_name=product.marketplace_product.title,
|
||||
requested=new_quantity,
|
||||
available=product.available_inventory
|
||||
available=product.available_inventory,
|
||||
)
|
||||
|
||||
existing_item.quantity = new_quantity
|
||||
@@ -206,16 +213,13 @@ class CartService:
|
||||
|
||||
logger.info(
|
||||
f"[CART_SERVICE] Updated existing cart item",
|
||||
extra={
|
||||
"cart_item_id": existing_item.id,
|
||||
"new_quantity": new_quantity
|
||||
}
|
||||
extra={"cart_item_id": existing_item.id, "new_quantity": new_quantity},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Product quantity updated in cart",
|
||||
"product_id": product_id,
|
||||
"quantity": new_quantity
|
||||
"quantity": new_quantity,
|
||||
}
|
||||
else:
|
||||
# Check inventory for new item
|
||||
@@ -225,14 +229,14 @@ class CartService:
|
||||
extra={
|
||||
"product_id": product_id,
|
||||
"requested": quantity,
|
||||
"available": product.available_inventory
|
||||
}
|
||||
"available": product.available_inventory,
|
||||
},
|
||||
)
|
||||
raise InsufficientInventoryForCartException(
|
||||
product_id=product_id,
|
||||
product_name=product.marketplace_product.title,
|
||||
requested=quantity,
|
||||
available=product.available_inventory
|
||||
available=product.available_inventory,
|
||||
)
|
||||
|
||||
# Create new cart item
|
||||
@@ -241,7 +245,7 @@ class CartService:
|
||||
session_id=session_id,
|
||||
product_id=product_id,
|
||||
quantity=quantity,
|
||||
price_at_add=current_price
|
||||
price_at_add=current_price,
|
||||
)
|
||||
db.add(cart_item)
|
||||
db.commit()
|
||||
@@ -252,23 +256,23 @@ class CartService:
|
||||
extra={
|
||||
"cart_item_id": cart_item.id,
|
||||
"quantity": quantity,
|
||||
"price": current_price
|
||||
}
|
||||
"price": current_price,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Product added to cart",
|
||||
"product_id": product_id,
|
||||
"quantity": quantity
|
||||
"quantity": quantity,
|
||||
}
|
||||
|
||||
def update_cart_item(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
product_id: int,
|
||||
quantity: int
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
product_id: int,
|
||||
quantity: int,
|
||||
) -> Dict:
|
||||
"""
|
||||
Update quantity of item in cart.
|
||||
@@ -292,25 +296,35 @@ class CartService:
|
||||
raise InvalidCartQuantityException(quantity=quantity, min_quantity=1)
|
||||
|
||||
# Find cart item
|
||||
cart_item = db.query(CartItem).filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id
|
||||
cart_item = (
|
||||
db.query(CartItem)
|
||||
.filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not cart_item:
|
||||
raise CartItemNotFoundException(product_id=product_id, session_id=session_id)
|
||||
raise CartItemNotFoundException(
|
||||
product_id=product_id, session_id=session_id
|
||||
)
|
||||
|
||||
# Verify product still exists and is active
|
||||
product = db.query(Product).filter(
|
||||
and_(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
and_(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not product:
|
||||
raise ProductNotFoundException(str(product_id))
|
||||
@@ -321,7 +335,7 @@ class CartService:
|
||||
product_id=product_id,
|
||||
product_name=product.marketplace_product.title,
|
||||
requested=quantity,
|
||||
available=product.available_inventory
|
||||
available=product.available_inventory,
|
||||
)
|
||||
|
||||
# Update quantity
|
||||
@@ -334,22 +348,18 @@ class CartService:
|
||||
extra={
|
||||
"cart_item_id": cart_item.id,
|
||||
"product_id": product_id,
|
||||
"new_quantity": quantity
|
||||
}
|
||||
"new_quantity": quantity,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Cart updated",
|
||||
"product_id": product_id,
|
||||
"quantity": quantity
|
||||
"quantity": quantity,
|
||||
}
|
||||
|
||||
def remove_from_cart(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str,
|
||||
product_id: int
|
||||
self, db: Session, vendor_id: int, session_id: str, product_id: int
|
||||
) -> Dict:
|
||||
"""
|
||||
Remove item from cart.
|
||||
@@ -367,16 +377,22 @@ class CartService:
|
||||
ProductNotFoundException: If product not in cart
|
||||
"""
|
||||
# Find and delete cart item
|
||||
cart_item = db.query(CartItem).filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id
|
||||
cart_item = (
|
||||
db.query(CartItem)
|
||||
.filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id,
|
||||
CartItem.product_id == product_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not cart_item:
|
||||
raise CartItemNotFoundException(product_id=product_id, session_id=session_id)
|
||||
raise CartItemNotFoundException(
|
||||
product_id=product_id, session_id=session_id
|
||||
)
|
||||
|
||||
db.delete(cart_item)
|
||||
db.commit()
|
||||
@@ -386,21 +402,13 @@ class CartService:
|
||||
extra={
|
||||
"cart_item_id": cart_item.id,
|
||||
"product_id": product_id,
|
||||
"session_id": session_id
|
||||
}
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Item removed from cart",
|
||||
"product_id": product_id
|
||||
}
|
||||
return {"message": "Item removed from cart", "product_id": product_id}
|
||||
|
||||
def clear_cart(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
session_id: str
|
||||
) -> Dict:
|
||||
def clear_cart(self, db: Session, vendor_id: int, session_id: str) -> Dict:
|
||||
"""
|
||||
Clear all items from cart.
|
||||
|
||||
@@ -413,12 +421,13 @@ class CartService:
|
||||
Success message with count of items removed
|
||||
"""
|
||||
# Delete all cart items for this session
|
||||
deleted_count = db.query(CartItem).filter(
|
||||
and_(
|
||||
CartItem.vendor_id == vendor_id,
|
||||
CartItem.session_id == session_id
|
||||
deleted_count = (
|
||||
db.query(CartItem)
|
||||
.filter(
|
||||
and_(CartItem.vendor_id == vendor_id, CartItem.session_id == session_id)
|
||||
)
|
||||
).delete()
|
||||
.delete()
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -427,14 +436,11 @@ class CartService:
|
||||
extra={
|
||||
"session_id": session_id,
|
||||
"vendor_id": vendor_id,
|
||||
"items_removed": deleted_count
|
||||
}
|
||||
"items_removed": deleted_count,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Cart cleared",
|
||||
"items_removed": deleted_count
|
||||
}
|
||||
return {"message": "Cart cleared", "items_removed": deleted_count}
|
||||
|
||||
|
||||
# Create service instance
|
||||
|
||||
@@ -3,22 +3,20 @@ Code Quality Service
|
||||
Business logic for managing architecture scans and violations
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from app.models.architecture_scan import (
|
||||
ArchitectureScan,
|
||||
ArchitectureViolation,
|
||||
ArchitectureRule,
|
||||
ViolationAssignment,
|
||||
ViolationComment
|
||||
)
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.architecture_scan import (ArchitectureRule, ArchitectureScan,
|
||||
ArchitectureViolation,
|
||||
ViolationAssignment,
|
||||
ViolationComment)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
class CodeQualityService:
|
||||
"""Service for managing code quality scans and violations"""
|
||||
|
||||
def run_scan(self, db: Session, triggered_by: str = 'manual') -> ArchitectureScan:
|
||||
def run_scan(self, db: Session, triggered_by: str = "manual") -> ArchitectureScan:
|
||||
"""
|
||||
Run architecture validator and store results in database
|
||||
|
||||
@@ -49,10 +47,10 @@ class CodeQualityService:
|
||||
start_time = datetime.now()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python', 'scripts/validate_architecture.py', '--json'],
|
||||
["python", "scripts/validate_architecture.py", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 minute timeout
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Architecture scan timed out after 5 minutes")
|
||||
@@ -63,17 +61,17 @@ class CodeQualityService:
|
||||
# Parse JSON output (get only the JSON part, skip progress messages)
|
||||
try:
|
||||
# Find the JSON part in stdout
|
||||
lines = result.stdout.strip().split('\n')
|
||||
lines = result.stdout.strip().split("\n")
|
||||
json_start = -1
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('{'):
|
||||
if line.strip().startswith("{"):
|
||||
json_start = i
|
||||
break
|
||||
|
||||
if json_start == -1:
|
||||
raise ValueError("No JSON output found")
|
||||
|
||||
json_output = '\n'.join(lines[json_start:])
|
||||
json_output = "\n".join(lines[json_start:])
|
||||
data = json.loads(json_output)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.error(f"Failed to parse validator output: {e}")
|
||||
@@ -84,33 +82,33 @@ class CodeQualityService:
|
||||
# Create scan record
|
||||
scan = ArchitectureScan(
|
||||
timestamp=datetime.now(),
|
||||
total_files=data.get('files_checked', 0),
|
||||
total_violations=data.get('total_violations', 0),
|
||||
errors=data.get('errors', 0),
|
||||
warnings=data.get('warnings', 0),
|
||||
total_files=data.get("files_checked", 0),
|
||||
total_violations=data.get("total_violations", 0),
|
||||
errors=data.get("errors", 0),
|
||||
warnings=data.get("warnings", 0),
|
||||
duration_seconds=duration,
|
||||
triggered_by=triggered_by,
|
||||
git_commit_hash=git_commit
|
||||
git_commit_hash=git_commit,
|
||||
)
|
||||
db.add(scan)
|
||||
db.flush() # Get scan.id
|
||||
|
||||
# Create violation records
|
||||
violations_data = data.get('violations', [])
|
||||
violations_data = data.get("violations", [])
|
||||
logger.info(f"Creating {len(violations_data)} violation records")
|
||||
|
||||
for v in violations_data:
|
||||
violation = ArchitectureViolation(
|
||||
scan_id=scan.id,
|
||||
rule_id=v['rule_id'],
|
||||
rule_name=v['rule_name'],
|
||||
severity=v['severity'],
|
||||
file_path=v['file_path'],
|
||||
line_number=v['line_number'],
|
||||
message=v['message'],
|
||||
context=v.get('context', ''),
|
||||
suggestion=v.get('suggestion', ''),
|
||||
status='open'
|
||||
rule_id=v["rule_id"],
|
||||
rule_name=v["rule_name"],
|
||||
severity=v["severity"],
|
||||
file_path=v["file_path"],
|
||||
line_number=v["line_number"],
|
||||
message=v["message"],
|
||||
context=v.get("context", ""),
|
||||
suggestion=v.get("suggestion", ""),
|
||||
status="open",
|
||||
)
|
||||
db.add(violation)
|
||||
|
||||
@@ -122,7 +120,11 @@ class CodeQualityService:
|
||||
|
||||
def get_latest_scan(self, db: Session) -> Optional[ArchitectureScan]:
|
||||
"""Get the most recent scan"""
|
||||
return db.query(ArchitectureScan).order_by(desc(ArchitectureScan.timestamp)).first()
|
||||
return (
|
||||
db.query(ArchitectureScan)
|
||||
.order_by(desc(ArchitectureScan.timestamp))
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_scan_by_id(self, db: Session, scan_id: int) -> Optional[ArchitectureScan]:
|
||||
"""Get scan by ID"""
|
||||
@@ -139,10 +141,12 @@ class CodeQualityService:
|
||||
Returns:
|
||||
List of ArchitectureScan objects, newest first
|
||||
"""
|
||||
return db.query(ArchitectureScan)\
|
||||
.order_by(desc(ArchitectureScan.timestamp))\
|
||||
.limit(limit)\
|
||||
return (
|
||||
db.query(ArchitectureScan)
|
||||
.order_by(desc(ArchitectureScan.timestamp))
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_violations(
|
||||
self,
|
||||
@@ -153,7 +157,7 @@ class CodeQualityService:
|
||||
rule_id: str = None,
|
||||
file_path: str = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0
|
||||
offset: int = 0,
|
||||
) -> Tuple[List[ArchitectureViolation], int]:
|
||||
"""
|
||||
Get violations with filtering and pagination
|
||||
@@ -194,24 +198,32 @@ class CodeQualityService:
|
||||
query = query.filter(ArchitectureViolation.rule_id == rule_id)
|
||||
|
||||
if file_path:
|
||||
query = query.filter(ArchitectureViolation.file_path.like(f'%{file_path}%'))
|
||||
query = query.filter(ArchitectureViolation.file_path.like(f"%{file_path}%"))
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Get page of results
|
||||
violations = query.order_by(
|
||||
ArchitectureViolation.severity.desc(),
|
||||
ArchitectureViolation.file_path
|
||||
).limit(limit).offset(offset).all()
|
||||
violations = (
|
||||
query.order_by(
|
||||
ArchitectureViolation.severity.desc(), ArchitectureViolation.file_path
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
|
||||
return violations, total
|
||||
|
||||
def get_violation_by_id(self, db: Session, violation_id: int) -> Optional[ArchitectureViolation]:
|
||||
def get_violation_by_id(
|
||||
self, db: Session, violation_id: int
|
||||
) -> Optional[ArchitectureViolation]:
|
||||
"""Get single violation with details"""
|
||||
return db.query(ArchitectureViolation).filter(
|
||||
ArchitectureViolation.id == violation_id
|
||||
).first()
|
||||
return (
|
||||
db.query(ArchitectureViolation)
|
||||
.filter(ArchitectureViolation.id == violation_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def assign_violation(
|
||||
self,
|
||||
@@ -220,7 +232,7 @@ class CodeQualityService:
|
||||
user_id: int,
|
||||
assigned_by: int,
|
||||
due_date: datetime = None,
|
||||
priority: str = 'medium'
|
||||
priority: str = "medium",
|
||||
) -> ViolationAssignment:
|
||||
"""
|
||||
Assign violation to a developer
|
||||
@@ -239,7 +251,7 @@ class CodeQualityService:
|
||||
# Update violation status
|
||||
violation = self.get_violation_by_id(db, violation_id)
|
||||
if violation:
|
||||
violation.status = 'assigned'
|
||||
violation.status = "assigned"
|
||||
violation.assigned_to = user_id
|
||||
|
||||
# Create assignment record
|
||||
@@ -248,7 +260,7 @@ class CodeQualityService:
|
||||
user_id=user_id,
|
||||
assigned_by=assigned_by,
|
||||
due_date=due_date,
|
||||
priority=priority
|
||||
priority=priority,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
@@ -257,11 +269,7 @@ class CodeQualityService:
|
||||
return assignment
|
||||
|
||||
def resolve_violation(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
resolved_by: int,
|
||||
resolution_note: str
|
||||
self, db: Session, violation_id: int, resolved_by: int, resolution_note: str
|
||||
) -> ArchitectureViolation:
|
||||
"""
|
||||
Mark violation as resolved
|
||||
@@ -279,7 +287,7 @@ class CodeQualityService:
|
||||
if not violation:
|
||||
raise ValueError(f"Violation {violation_id} not found")
|
||||
|
||||
violation.status = 'resolved'
|
||||
violation.status = "resolved"
|
||||
violation.resolved_at = datetime.now()
|
||||
violation.resolved_by = resolved_by
|
||||
violation.resolution_note = resolution_note
|
||||
@@ -289,11 +297,7 @@ class CodeQualityService:
|
||||
return violation
|
||||
|
||||
def ignore_violation(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
ignored_by: int,
|
||||
reason: str
|
||||
self, db: Session, violation_id: int, ignored_by: int, reason: str
|
||||
) -> ArchitectureViolation:
|
||||
"""
|
||||
Mark violation as ignored/won't fix
|
||||
@@ -311,7 +315,7 @@ class CodeQualityService:
|
||||
if not violation:
|
||||
raise ValueError(f"Violation {violation_id} not found")
|
||||
|
||||
violation.status = 'ignored'
|
||||
violation.status = "ignored"
|
||||
violation.resolved_at = datetime.now()
|
||||
violation.resolved_by = ignored_by
|
||||
violation.resolution_note = f"Ignored: {reason}"
|
||||
@@ -321,11 +325,7 @@ class CodeQualityService:
|
||||
return violation
|
||||
|
||||
def add_comment(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
user_id: int,
|
||||
comment: str
|
||||
self, db: Session, violation_id: int, user_id: int, comment: str
|
||||
) -> ViolationComment:
|
||||
"""
|
||||
Add comment to violation
|
||||
@@ -340,9 +340,7 @@ class CodeQualityService:
|
||||
ViolationComment object
|
||||
"""
|
||||
comment_obj = ViolationComment(
|
||||
violation_id=violation_id,
|
||||
user_id=user_id,
|
||||
comment=comment
|
||||
violation_id=violation_id, user_id=user_id, comment=comment
|
||||
)
|
||||
db.add(comment_obj)
|
||||
db.commit()
|
||||
@@ -360,79 +358,95 @@ class CodeQualityService:
|
||||
latest_scan = self.get_latest_scan(db)
|
||||
if not latest_scan:
|
||||
return {
|
||||
'total_violations': 0,
|
||||
'errors': 0,
|
||||
'warnings': 0,
|
||||
'open': 0,
|
||||
'assigned': 0,
|
||||
'resolved': 0,
|
||||
'ignored': 0,
|
||||
'technical_debt_score': 100,
|
||||
'trend': [],
|
||||
'by_severity': {},
|
||||
'by_rule': {},
|
||||
'by_module': {},
|
||||
'top_files': []
|
||||
"total_violations": 0,
|
||||
"errors": 0,
|
||||
"warnings": 0,
|
||||
"open": 0,
|
||||
"assigned": 0,
|
||||
"resolved": 0,
|
||||
"ignored": 0,
|
||||
"technical_debt_score": 100,
|
||||
"trend": [],
|
||||
"by_severity": {},
|
||||
"by_rule": {},
|
||||
"by_module": {},
|
||||
"top_files": [],
|
||||
}
|
||||
|
||||
# Get violation counts by status
|
||||
status_counts = db.query(
|
||||
ArchitectureViolation.status,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.status).all()
|
||||
status_counts = (
|
||||
db.query(ArchitectureViolation.status, func.count(ArchitectureViolation.id))
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.status)
|
||||
.all()
|
||||
)
|
||||
|
||||
status_dict = {status: count for status, count in status_counts}
|
||||
|
||||
# Get violations by severity
|
||||
severity_counts = db.query(
|
||||
ArchitectureViolation.severity,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.severity).all()
|
||||
severity_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.severity, func.count(ArchitectureViolation.id)
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.severity)
|
||||
.all()
|
||||
)
|
||||
|
||||
by_severity = {sev: count for sev, count in severity_counts}
|
||||
|
||||
# Get violations by rule
|
||||
rule_counts = db.query(
|
||||
ArchitectureViolation.rule_id,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.rule_id).all()
|
||||
rule_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.rule_id, func.count(ArchitectureViolation.id)
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.rule_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
by_rule = {rule: count for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10]}
|
||||
by_rule = {
|
||||
rule: count
|
||||
for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[
|
||||
:10
|
||||
]
|
||||
}
|
||||
|
||||
# Get top violating files
|
||||
file_counts = db.query(
|
||||
ArchitectureViolation.file_path,
|
||||
func.count(ArchitectureViolation.id).label('count')
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.file_path)\
|
||||
.order_by(desc('count'))\
|
||||
.limit(10).all()
|
||||
file_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.file_path,
|
||||
func.count(ArchitectureViolation.id).label("count"),
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.file_path)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
top_files = [{'file': file, 'count': count} for file, count in file_counts]
|
||||
top_files = [{"file": file, "count": count} for file, count in file_counts]
|
||||
|
||||
# Get violations by module (extract module from file path)
|
||||
by_module = {}
|
||||
violations = db.query(ArchitectureViolation.file_path).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).all()
|
||||
violations = (
|
||||
db.query(ArchitectureViolation.file_path)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
for v in violations:
|
||||
path_parts = v.file_path.split('/')
|
||||
path_parts = v.file_path.split("/")
|
||||
if len(path_parts) >= 2:
|
||||
module = '/'.join(path_parts[:2]) # e.g., 'app/api'
|
||||
module = "/".join(path_parts[:2]) # e.g., 'app/api'
|
||||
else:
|
||||
module = path_parts[0]
|
||||
by_module[module] = by_module.get(module, 0) + 1
|
||||
|
||||
# Sort by count and take top 10
|
||||
by_module = dict(sorted(by_module.items(), key=lambda x: x[1], reverse=True)[:10])
|
||||
by_module = dict(
|
||||
sorted(by_module.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
)
|
||||
|
||||
# Calculate technical debt score
|
||||
tech_debt_score = self.calculate_technical_debt_score(db, latest_scan.id)
|
||||
@@ -441,29 +455,29 @@ class CodeQualityService:
|
||||
trend_scans = self.get_scan_history(db, limit=7)
|
||||
trend = [
|
||||
{
|
||||
'timestamp': scan.timestamp.isoformat(),
|
||||
'violations': scan.total_violations,
|
||||
'errors': scan.errors,
|
||||
'warnings': scan.warnings
|
||||
"timestamp": scan.timestamp.isoformat(),
|
||||
"violations": scan.total_violations,
|
||||
"errors": scan.errors,
|
||||
"warnings": scan.warnings,
|
||||
}
|
||||
for scan in reversed(trend_scans) # Oldest first for chart
|
||||
]
|
||||
|
||||
return {
|
||||
'total_violations': latest_scan.total_violations,
|
||||
'errors': latest_scan.errors,
|
||||
'warnings': latest_scan.warnings,
|
||||
'open': status_dict.get('open', 0),
|
||||
'assigned': status_dict.get('assigned', 0),
|
||||
'resolved': status_dict.get('resolved', 0),
|
||||
'ignored': status_dict.get('ignored', 0),
|
||||
'technical_debt_score': tech_debt_score,
|
||||
'trend': trend,
|
||||
'by_severity': by_severity,
|
||||
'by_rule': by_rule,
|
||||
'by_module': by_module,
|
||||
'top_files': top_files,
|
||||
'last_scan': latest_scan.timestamp.isoformat() if latest_scan else None
|
||||
"total_violations": latest_scan.total_violations,
|
||||
"errors": latest_scan.errors,
|
||||
"warnings": latest_scan.warnings,
|
||||
"open": status_dict.get("open", 0),
|
||||
"assigned": status_dict.get("assigned", 0),
|
||||
"resolved": status_dict.get("resolved", 0),
|
||||
"ignored": status_dict.get("ignored", 0),
|
||||
"technical_debt_score": tech_debt_score,
|
||||
"trend": trend,
|
||||
"by_severity": by_severity,
|
||||
"by_rule": by_rule,
|
||||
"by_module": by_module,
|
||||
"top_files": top_files,
|
||||
"last_scan": latest_scan.timestamp.isoformat() if latest_scan else None,
|
||||
}
|
||||
|
||||
def calculate_technical_debt_score(self, db: Session, scan_id: int = None) -> int:
|
||||
@@ -497,10 +511,7 @@ class CodeQualityService:
|
||||
"""Get current git commit hash"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'rev-parse', 'HEAD'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()[:40]
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Configuration management services
|
||||
# Configuration management services
|
||||
|
||||
@@ -19,8 +19,9 @@ This allows:
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.database.content_page import ContentPage
|
||||
|
||||
@@ -35,7 +36,7 @@ class ContentPageService:
|
||||
db: Session,
|
||||
slug: str,
|
||||
vendor_id: Optional[int] = None,
|
||||
include_unpublished: bool = False
|
||||
include_unpublished: bool = False,
|
||||
) -> Optional[ContentPage]:
|
||||
"""
|
||||
Get content page for a vendor with fallback to platform default.
|
||||
@@ -62,28 +63,20 @@ class ContentPageService:
|
||||
if vendor_id:
|
||||
vendor_page = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == vendor_id,
|
||||
*filters
|
||||
)
|
||||
)
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
|
||||
.first()
|
||||
)
|
||||
|
||||
if vendor_page:
|
||||
logger.debug(f"Found vendor-specific page: {slug} for vendor_id={vendor_id}")
|
||||
logger.debug(
|
||||
f"Found vendor-specific page: {slug} for vendor_id={vendor_id}"
|
||||
)
|
||||
return vendor_page
|
||||
|
||||
# Fallback to platform default
|
||||
platform_page = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == None,
|
||||
*filters
|
||||
)
|
||||
)
|
||||
.filter(and_(ContentPage.vendor_id == None, *filters))
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -100,7 +93,7 @@ class ContentPageService:
|
||||
vendor_id: Optional[int] = None,
|
||||
include_unpublished: bool = False,
|
||||
footer_only: bool = False,
|
||||
header_only: bool = False
|
||||
header_only: bool = False,
|
||||
) -> List[ContentPage]:
|
||||
"""
|
||||
List all available pages for a vendor (includes vendor overrides + platform defaults).
|
||||
@@ -133,12 +126,7 @@ class ContentPageService:
|
||||
if vendor_id:
|
||||
vendor_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == vendor_id,
|
||||
*filters
|
||||
)
|
||||
)
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
@@ -146,12 +134,7 @@ class ContentPageService:
|
||||
# Get platform defaults
|
||||
platform_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == None,
|
||||
*filters
|
||||
)
|
||||
)
|
||||
.filter(and_(ContentPage.vendor_id == None, *filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
@@ -159,8 +142,7 @@ class ContentPageService:
|
||||
# Merge: vendor overrides take precedence
|
||||
vendor_slugs = {page.slug for page in vendor_pages}
|
||||
all_pages = vendor_pages + [
|
||||
page for page in platform_pages
|
||||
if page.slug not in vendor_slugs
|
||||
page for page in platform_pages if page.slug not in vendor_slugs
|
||||
]
|
||||
|
||||
# Sort by display_order
|
||||
@@ -183,7 +165,7 @@ class ContentPageService:
|
||||
show_in_footer: bool = True,
|
||||
show_in_header: bool = False,
|
||||
display_order: int = 0,
|
||||
created_by: Optional[int] = None
|
||||
created_by: Optional[int] = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Create a new content page.
|
||||
@@ -229,7 +211,9 @@ class ContentPageService:
|
||||
db.commit()
|
||||
db.refresh(page)
|
||||
|
||||
logger.info(f"Created content page: {slug} (vendor_id={vendor_id}, id={page.id})")
|
||||
logger.info(
|
||||
f"Created content page: {slug} (vendor_id={vendor_id}, id={page.id})"
|
||||
)
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
@@ -246,7 +230,7 @@ class ContentPageService:
|
||||
show_in_footer: Optional[bool] = None,
|
||||
show_in_header: Optional[bool] = None,
|
||||
display_order: Optional[int] = None,
|
||||
updated_by: Optional[int] = None
|
||||
updated_by: Optional[int] = None,
|
||||
) -> Optional[ContentPage]:
|
||||
"""
|
||||
Update an existing content page.
|
||||
@@ -338,9 +322,7 @@ class ContentPageService:
|
||||
|
||||
@staticmethod
|
||||
def list_all_vendor_pages(
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
include_unpublished: bool = False
|
||||
db: Session, vendor_id: int, include_unpublished: bool = False
|
||||
) -> List[ContentPage]:
|
||||
"""
|
||||
List only vendor-specific pages (no platform defaults).
|
||||
@@ -367,8 +349,7 @@ class ContentPageService:
|
||||
|
||||
@staticmethod
|
||||
def list_all_platform_pages(
|
||||
db: Session,
|
||||
include_unpublished: bool = False
|
||||
db: Session, include_unpublished: bool = False
|
||||
) -> List[ContentPage]:
|
||||
"""
|
||||
List only platform default pages.
|
||||
|
||||
@@ -8,24 +8,24 @@ with complete vendor isolation.
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions.customer import (CustomerAlreadyExistsException,
|
||||
CustomerNotActiveException,
|
||||
CustomerNotFoundException,
|
||||
CustomerValidationException,
|
||||
DuplicateCustomerEmailException,
|
||||
InvalidCustomerCredentialsException)
|
||||
from app.exceptions.vendor import (VendorNotActiveException,
|
||||
VendorNotFoundException)
|
||||
from app.services.auth_service import AuthService
|
||||
from models.database.customer import Customer, CustomerAddress
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.customer import CustomerRegister, CustomerUpdate
|
||||
from models.schema.auth import UserLogin
|
||||
from app.exceptions.customer import (
|
||||
CustomerNotFoundException,
|
||||
CustomerAlreadyExistsException,
|
||||
CustomerNotActiveException,
|
||||
InvalidCustomerCredentialsException,
|
||||
CustomerValidationException,
|
||||
DuplicateCustomerEmailException
|
||||
)
|
||||
from app.exceptions.vendor import VendorNotFoundException, VendorNotActiveException
|
||||
from app.services.auth_service import AuthService
|
||||
from models.schema.customer import CustomerRegister, CustomerUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,10 +37,7 @@ class CustomerService:
|
||||
self.auth_service = AuthService()
|
||||
|
||||
def register_customer(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_data: CustomerRegister
|
||||
self, db: Session, vendor_id: int, customer_data: CustomerRegister
|
||||
) -> Customer:
|
||||
"""
|
||||
Register a new customer for a specific vendor.
|
||||
@@ -68,18 +65,26 @@ class CustomerService:
|
||||
raise VendorNotActiveException(vendor.vendor_code)
|
||||
|
||||
# Check if email already exists for this vendor
|
||||
existing_customer = db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == customer_data.email.lower()
|
||||
existing_customer = (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == customer_data.email.lower(),
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_customer:
|
||||
raise DuplicateCustomerEmailException(customer_data.email, vendor.vendor_code)
|
||||
raise DuplicateCustomerEmailException(
|
||||
customer_data.email, vendor.vendor_code
|
||||
)
|
||||
|
||||
# Generate unique customer number for this vendor
|
||||
customer_number = self._generate_customer_number(db, vendor_id, vendor.vendor_code)
|
||||
customer_number = self._generate_customer_number(
|
||||
db, vendor_id, vendor.vendor_code
|
||||
)
|
||||
|
||||
# Hash password
|
||||
hashed_password = self.auth_service.hash_password(customer_data.password)
|
||||
@@ -93,8 +98,12 @@ class CustomerService:
|
||||
last_name=customer_data.last_name,
|
||||
phone=customer_data.phone,
|
||||
customer_number=customer_number,
|
||||
marketing_consent=customer_data.marketing_consent if hasattr(customer_data, 'marketing_consent') else False,
|
||||
is_active=True
|
||||
marketing_consent=(
|
||||
customer_data.marketing_consent
|
||||
if hasattr(customer_data, "marketing_consent")
|
||||
else False
|
||||
),
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -114,15 +123,11 @@ class CustomerService:
|
||||
db.rollback()
|
||||
logger.error(f"Error registering customer: {str(e)}")
|
||||
raise CustomerValidationException(
|
||||
message="Failed to register customer",
|
||||
details={"error": str(e)}
|
||||
message="Failed to register customer", details={"error": str(e)}
|
||||
)
|
||||
|
||||
def login_customer(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
credentials: UserLogin
|
||||
self, db: Session, vendor_id: int, credentials: UserLogin
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Authenticate customer and generate JWT token.
|
||||
@@ -146,20 +151,23 @@ class CustomerService:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Find customer by email (vendor-scoped)
|
||||
customer = db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == credentials.email_or_username.lower()
|
||||
customer = (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == credentials.email_or_username.lower(),
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not customer:
|
||||
raise InvalidCustomerCredentialsException()
|
||||
|
||||
# Verify password using auth_manager directly
|
||||
if not self.auth_service.auth_manager.verify_password(
|
||||
credentials.password,
|
||||
customer.hashed_password
|
||||
credentials.password, customer.hashed_password
|
||||
):
|
||||
raise InvalidCustomerCredentialsException()
|
||||
|
||||
@@ -170,6 +178,7 @@ class CustomerService:
|
||||
# Generate JWT token with customer context
|
||||
# Use auth_manager directly since Customer is not a User model
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from jose import jwt
|
||||
|
||||
auth_manager = self.auth_service.auth_manager
|
||||
@@ -185,7 +194,9 @@ class CustomerService:
|
||||
"iat": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, auth_manager.secret_key, algorithm=auth_manager.algorithm)
|
||||
token = jwt.encode(
|
||||
payload, auth_manager.secret_key, algorithm=auth_manager.algorithm
|
||||
)
|
||||
|
||||
token_data = {
|
||||
"access_token": token,
|
||||
@@ -198,17 +209,9 @@ class CustomerService:
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return {
|
||||
"customer": customer,
|
||||
"token_data": token_data
|
||||
}
|
||||
return {"customer": customer, "token_data": token_data}
|
||||
|
||||
def get_customer(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int
|
||||
) -> Customer:
|
||||
def get_customer(self, db: Session, vendor_id: int, customer_id: int) -> Customer:
|
||||
"""
|
||||
Get customer by ID with vendor isolation.
|
||||
|
||||
@@ -223,12 +226,11 @@ class CustomerService:
|
||||
Raises:
|
||||
CustomerNotFoundException: If customer not found
|
||||
"""
|
||||
customer = db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.id == customer_id,
|
||||
Customer.vendor_id == vendor_id
|
||||
)
|
||||
).first()
|
||||
customer = (
|
||||
db.query(Customer)
|
||||
.filter(and_(Customer.id == customer_id, Customer.vendor_id == vendor_id))
|
||||
.first()
|
||||
)
|
||||
|
||||
if not customer:
|
||||
raise CustomerNotFoundException(str(customer_id))
|
||||
@@ -236,10 +238,7 @@ class CustomerService:
|
||||
return customer
|
||||
|
||||
def get_customer_by_email(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
email: str
|
||||
self, db: Session, vendor_id: int, email: str
|
||||
) -> Optional[Customer]:
|
||||
"""
|
||||
Get customer by email (vendor-scoped).
|
||||
@@ -252,19 +251,20 @@ class CustomerService:
|
||||
Returns:
|
||||
Optional[Customer]: Customer object or None
|
||||
"""
|
||||
return db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == email.lower()
|
||||
return (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(Customer.vendor_id == vendor_id, Customer.email == email.lower())
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
def update_customer(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
customer_data: CustomerUpdate
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
customer_data: CustomerUpdate,
|
||||
) -> Customer:
|
||||
"""
|
||||
Update customer profile.
|
||||
@@ -290,13 +290,17 @@ class CustomerService:
|
||||
for field, value in update_data.items():
|
||||
if field == "email" and value:
|
||||
# Check if new email already exists for this vendor
|
||||
existing = db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == value.lower(),
|
||||
Customer.id != customer_id
|
||||
existing = (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.email == value.lower(),
|
||||
Customer.id != customer_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing:
|
||||
raise DuplicateCustomerEmailException(value, "vendor")
|
||||
@@ -317,15 +321,11 @@ class CustomerService:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating customer: {str(e)}")
|
||||
raise CustomerValidationException(
|
||||
message="Failed to update customer",
|
||||
details={"error": str(e)}
|
||||
message="Failed to update customer", details={"error": str(e)}
|
||||
)
|
||||
|
||||
def deactivate_customer(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int
|
||||
self, db: Session, vendor_id: int, customer_id: int
|
||||
) -> Customer:
|
||||
"""
|
||||
Deactivate customer account.
|
||||
@@ -352,10 +352,7 @@ class CustomerService:
|
||||
return customer
|
||||
|
||||
def update_customer_stats(
|
||||
self,
|
||||
db: Session,
|
||||
customer_id: int,
|
||||
order_total: float
|
||||
self, db: Session, customer_id: int, order_total: float
|
||||
) -> None:
|
||||
"""
|
||||
Update customer statistics after order.
|
||||
@@ -377,10 +374,7 @@ class CustomerService:
|
||||
logger.debug(f"Updated stats for customer {customer.email}")
|
||||
|
||||
def _generate_customer_number(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
vendor_code: str
|
||||
self, db: Session, vendor_id: int, vendor_code: str
|
||||
) -> str:
|
||||
"""
|
||||
Generate unique customer number for vendor.
|
||||
@@ -397,21 +391,23 @@ class CustomerService:
|
||||
str: Unique customer number
|
||||
"""
|
||||
# Get count of customers for this vendor
|
||||
count = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor_id
|
||||
).count()
|
||||
count = db.query(Customer).filter(Customer.vendor_id == vendor_id).count()
|
||||
|
||||
# Generate number with padding
|
||||
sequence = str(count + 1).zfill(5)
|
||||
customer_number = f"{vendor_code.upper()}-CUST-{sequence}"
|
||||
|
||||
# Ensure uniqueness (in case of deletions)
|
||||
while db.query(Customer).filter(
|
||||
while (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(
|
||||
Customer.vendor_id == vendor_id,
|
||||
Customer.customer_number == customer_number
|
||||
Customer.customer_number == customer_number,
|
||||
)
|
||||
).first():
|
||||
)
|
||||
.first()
|
||||
):
|
||||
count += 1
|
||||
sequence = str(count + 1).zfill(5)
|
||||
customer_number = f"{vendor_code.upper()}-CUST-{sequence}"
|
||||
|
||||
@@ -5,27 +5,20 @@ from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
InventoryNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
InventoryValidationException,
|
||||
NegativeInventoryException,
|
||||
InvalidQuantityException,
|
||||
ValidationException,
|
||||
ProductNotFoundException,
|
||||
)
|
||||
from models.schema.inventory import (
|
||||
InventoryCreate,
|
||||
InventoryAdjust,
|
||||
InventoryUpdate,
|
||||
InventoryReserve,
|
||||
InventoryLocationResponse,
|
||||
ProductInventorySummary
|
||||
)
|
||||
from app.exceptions import (InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
InvalidQuantityException,
|
||||
InventoryNotFoundException,
|
||||
InventoryValidationException,
|
||||
NegativeInventoryException,
|
||||
ProductNotFoundException, ValidationException)
|
||||
from models.database.inventory import Inventory
|
||||
from models.database.product import Product
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.inventory import (InventoryAdjust, InventoryCreate,
|
||||
InventoryLocationResponse,
|
||||
InventoryReserve, InventoryUpdate,
|
||||
ProductInventorySummary)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,7 +27,7 @@ class InventoryService:
|
||||
"""Service for inventory operations with vendor isolation."""
|
||||
|
||||
def set_inventory(
|
||||
self, db: Session, vendor_id: int, inventory_data: InventoryCreate
|
||||
self, db: Session, vendor_id: int, inventory_data: InventoryCreate
|
||||
) -> Inventory:
|
||||
"""
|
||||
Set exact inventory quantity for a product at a location (replaces existing).
|
||||
@@ -93,7 +86,11 @@ class InventoryService:
|
||||
)
|
||||
return new_inventory
|
||||
|
||||
except (ProductNotFoundException, InvalidQuantityException, InventoryValidationException):
|
||||
except (
|
||||
ProductNotFoundException,
|
||||
InvalidQuantityException,
|
||||
InventoryValidationException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -102,7 +99,7 @@ class InventoryService:
|
||||
raise ValidationException("Failed to set inventory")
|
||||
|
||||
def adjust_inventory(
|
||||
self, db: Session, vendor_id: int, inventory_data: InventoryAdjust
|
||||
self, db: Session, vendor_id: int, inventory_data: InventoryAdjust
|
||||
) -> Inventory:
|
||||
"""
|
||||
Adjust inventory by adding or removing quantity.
|
||||
@@ -124,7 +121,9 @@ class InventoryService:
|
||||
location = self._validate_location(inventory_data.location)
|
||||
|
||||
# Check if inventory exists
|
||||
existing = self._get_inventory_entry(db, inventory_data.product_id, location)
|
||||
existing = self._get_inventory_entry(
|
||||
db, inventory_data.product_id, location
|
||||
)
|
||||
|
||||
if not existing:
|
||||
# Create new if adding, error if removing
|
||||
@@ -173,8 +172,12 @@ class InventoryService:
|
||||
)
|
||||
return existing
|
||||
|
||||
except (ProductNotFoundException, InventoryNotFoundException,
|
||||
InsufficientInventoryException, InventoryValidationException):
|
||||
except (
|
||||
ProductNotFoundException,
|
||||
InventoryNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
InventoryValidationException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -183,7 +186,7 @@ class InventoryService:
|
||||
raise ValidationException("Failed to adjust inventory")
|
||||
|
||||
def reserve_inventory(
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
) -> Inventory:
|
||||
"""
|
||||
Reserve inventory for an order (increases reserved_quantity).
|
||||
@@ -231,8 +234,12 @@ class InventoryService:
|
||||
)
|
||||
return inventory
|
||||
|
||||
except (ProductNotFoundException, InventoryNotFoundException,
|
||||
InsufficientInventoryException, InvalidQuantityException):
|
||||
except (
|
||||
ProductNotFoundException,
|
||||
InventoryNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
InvalidQuantityException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -241,7 +248,7 @@ class InventoryService:
|
||||
raise ValidationException("Failed to reserve inventory")
|
||||
|
||||
def release_reservation(
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
) -> Inventory:
|
||||
"""
|
||||
Release reserved inventory (decreases reserved_quantity).
|
||||
@@ -287,7 +294,11 @@ class InventoryService:
|
||||
)
|
||||
return inventory
|
||||
|
||||
except (ProductNotFoundException, InventoryNotFoundException, InvalidQuantityException):
|
||||
except (
|
||||
ProductNotFoundException,
|
||||
InventoryNotFoundException,
|
||||
InvalidQuantityException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -296,7 +307,7 @@ class InventoryService:
|
||||
raise ValidationException("Failed to release reservation")
|
||||
|
||||
def fulfill_reservation(
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
self, db: Session, vendor_id: int, reserve_data: InventoryReserve
|
||||
) -> Inventory:
|
||||
"""
|
||||
Fulfill a reservation (decreases both quantity and reserved_quantity).
|
||||
@@ -349,8 +360,12 @@ class InventoryService:
|
||||
)
|
||||
return inventory
|
||||
|
||||
except (ProductNotFoundException, InventoryNotFoundException,
|
||||
InsufficientInventoryException, InvalidQuantityException):
|
||||
except (
|
||||
ProductNotFoundException,
|
||||
InventoryNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
InvalidQuantityException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -359,7 +374,7 @@ class InventoryService:
|
||||
raise ValidationException("Failed to fulfill reservation")
|
||||
|
||||
def get_product_inventory(
|
||||
self, db: Session, vendor_id: int, product_id: int
|
||||
self, db: Session, vendor_id: int, product_id: int
|
||||
) -> ProductInventorySummary:
|
||||
"""
|
||||
Get inventory summary for a product across all locations.
|
||||
@@ -376,9 +391,7 @@ class InventoryService:
|
||||
product = self._get_vendor_product(db, vendor_id, product_id)
|
||||
|
||||
inventory_entries = (
|
||||
db.query(Inventory)
|
||||
.filter(Inventory.product_id == product_id)
|
||||
.all()
|
||||
db.query(Inventory).filter(Inventory.product_id == product_id).all()
|
||||
)
|
||||
|
||||
if not inventory_entries:
|
||||
@@ -425,8 +438,13 @@ class InventoryService:
|
||||
raise ValidationException("Failed to retrieve product inventory")
|
||||
|
||||
def get_vendor_inventory(
|
||||
self, db: Session, vendor_id: int, skip: int = 0, limit: int = 100,
|
||||
location: Optional[str] = None, low_stock_threshold: Optional[int] = None
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
location: Optional[str] = None,
|
||||
low_stock_threshold: Optional[int] = None,
|
||||
) -> List[Inventory]:
|
||||
"""
|
||||
Get all inventory for a vendor with filtering.
|
||||
@@ -458,8 +476,11 @@ class InventoryService:
|
||||
raise ValidationException("Failed to retrieve vendor inventory")
|
||||
|
||||
def update_inventory(
|
||||
self, db: Session, vendor_id: int, inventory_id: int,
|
||||
inventory_update: InventoryUpdate
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
) -> Inventory:
|
||||
"""Update inventory entry."""
|
||||
try:
|
||||
@@ -475,7 +496,9 @@ class InventoryService:
|
||||
inventory.quantity = inventory_update.quantity
|
||||
|
||||
if inventory_update.reserved_quantity is not None:
|
||||
self._validate_quantity(inventory_update.reserved_quantity, allow_zero=True)
|
||||
self._validate_quantity(
|
||||
inventory_update.reserved_quantity, allow_zero=True
|
||||
)
|
||||
inventory.reserved_quantity = inventory_update.reserved_quantity
|
||||
|
||||
if inventory_update.location:
|
||||
@@ -488,7 +511,11 @@ class InventoryService:
|
||||
logger.info(f"Updated inventory {inventory_id}")
|
||||
return inventory
|
||||
|
||||
except (InventoryNotFoundException, InvalidQuantityException, InventoryValidationException):
|
||||
except (
|
||||
InventoryNotFoundException,
|
||||
InvalidQuantityException,
|
||||
InventoryValidationException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -496,9 +523,7 @@ class InventoryService:
|
||||
logger.error(f"Error updating inventory: {str(e)}")
|
||||
raise ValidationException("Failed to update inventory")
|
||||
|
||||
def delete_inventory(
|
||||
self, db: Session, vendor_id: int, inventory_id: int
|
||||
) -> bool:
|
||||
def delete_inventory(self, db: Session, vendor_id: int, inventory_id: int) -> bool:
|
||||
"""Delete inventory entry."""
|
||||
try:
|
||||
inventory = self._get_inventory_by_id(db, inventory_id)
|
||||
@@ -521,28 +546,30 @@ class InventoryService:
|
||||
raise ValidationException("Failed to delete inventory")
|
||||
|
||||
# Private helper methods
|
||||
def _get_vendor_product(self, db: Session, vendor_id: int, product_id: int) -> Product:
|
||||
def _get_vendor_product(
|
||||
self, db: Session, vendor_id: int, product_id: int
|
||||
) -> Product:
|
||||
"""Get product and verify it belongs to vendor."""
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id
|
||||
).first()
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(Product.id == product_id, Product.vendor_id == vendor_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not product:
|
||||
raise ProductNotFoundException(f"Product {product_id} not found in your catalog")
|
||||
raise ProductNotFoundException(
|
||||
f"Product {product_id} not found in your catalog"
|
||||
)
|
||||
|
||||
return product
|
||||
|
||||
def _get_inventory_entry(
|
||||
self, db: Session, product_id: int, location: str
|
||||
self, db: Session, product_id: int, location: str
|
||||
) -> Optional[Inventory]:
|
||||
"""Get inventory entry by product and location."""
|
||||
return (
|
||||
db.query(Inventory)
|
||||
.filter(
|
||||
Inventory.product_id == product_id,
|
||||
Inventory.location == location
|
||||
)
|
||||
.filter(Inventory.product_id == product_id, Inventory.location == location)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
@@ -5,20 +5,15 @@ from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException,
|
||||
ImportJobCannotBeCancelledException,
|
||||
ImportJobCannotBeDeletedException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schema.marketplace_import_job import (
|
||||
MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobRequest
|
||||
)
|
||||
from app.exceptions import (ImportJobCannotBeCancelledException,
|
||||
ImportJobCannotBeDeletedException,
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException, ValidationException)
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.marketplace_import_job import (MarketplaceImportJobRequest,
|
||||
MarketplaceImportJobResponse)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,7 +26,7 @@ class MarketplaceImportJobService:
|
||||
db: Session,
|
||||
request: MarketplaceImportJobRequest,
|
||||
vendor: Vendor, # CHANGED: Vendor object from middleware
|
||||
user: User
|
||||
user: User,
|
||||
) -> MarketplaceImportJob:
|
||||
"""
|
||||
Create a new marketplace import job.
|
||||
@@ -147,7 +142,9 @@ class MarketplaceImportJobService:
|
||||
marketplace=job.marketplace,
|
||||
vendor_id=job.vendor_id,
|
||||
vendor_code=job.vendor.vendor_code if job.vendor else None, # FIXED
|
||||
vendor_name=job.vendor.name if job.vendor else None, # FIXED: from relationship
|
||||
vendor_name=(
|
||||
job.vendor.name if job.vendor else None
|
||||
), # FIXED: from relationship
|
||||
source_url=job.source_url,
|
||||
imported=job.imported_count or 0,
|
||||
updated=job.updated_count or 0,
|
||||
|
||||
@@ -17,19 +17,20 @@ from typing import Generator, List, Optional, Tuple
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
MarketplaceProductNotFoundException,
|
||||
MarketplaceProductAlreadyExistsException,
|
||||
InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductValidationException,
|
||||
ValidationException,
|
||||
)
|
||||
from app.services.marketplace_import_job_service import marketplace_import_job_service
|
||||
from models.schema.marketplace_product import MarketplaceProductCreate, MarketplaceProductUpdate
|
||||
from models.schema.inventory import InventoryLocationResponse, InventorySummaryResponse
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.inventory import Inventory
|
||||
from app.exceptions import (InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductAlreadyExistsException,
|
||||
MarketplaceProductNotFoundException,
|
||||
MarketplaceProductValidationException,
|
||||
ValidationException)
|
||||
from app.services.marketplace_import_job_service import \
|
||||
marketplace_import_job_service
|
||||
from app.utils.data_processing import GTINProcessor, PriceProcessor
|
||||
from models.database.inventory import Inventory
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.schema.inventory import (InventoryLocationResponse,
|
||||
InventorySummaryResponse)
|
||||
from models.schema.marketplace_product import (MarketplaceProductCreate,
|
||||
MarketplaceProductUpdate)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,14 +43,18 @@ class MarketplaceProductService:
|
||||
self.gtin_processor = GTINProcessor()
|
||||
self.price_processor = PriceProcessor()
|
||||
|
||||
def create_product(self, db: Session, product_data: MarketplaceProductCreate) -> MarketplaceProduct:
|
||||
def create_product(
|
||||
self, db: Session, product_data: MarketplaceProductCreate
|
||||
) -> MarketplaceProduct:
|
||||
"""Create a new product with validation."""
|
||||
try:
|
||||
# Process and validate GTIN if provided
|
||||
if product_data.gtin:
|
||||
normalized_gtin = self.gtin_processor.normalize(product_data.gtin)
|
||||
if not normalized_gtin:
|
||||
raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin")
|
||||
raise InvalidMarketplaceProductDataException(
|
||||
"Invalid GTIN format", field="gtin"
|
||||
)
|
||||
product_data.gtin = normalized_gtin
|
||||
|
||||
# Process price if provided
|
||||
@@ -70,11 +75,18 @@ class MarketplaceProductService:
|
||||
product_data.marketplace = "Letzshop"
|
||||
|
||||
# Validate required fields
|
||||
if not product_data.marketplace_product_id or not product_data.marketplace_product_id.strip():
|
||||
raise MarketplaceProductValidationException("MarketplaceProduct ID is required", field="marketplace_product_id")
|
||||
if (
|
||||
not product_data.marketplace_product_id
|
||||
or not product_data.marketplace_product_id.strip()
|
||||
):
|
||||
raise MarketplaceProductValidationException(
|
||||
"MarketplaceProduct ID is required", field="marketplace_product_id"
|
||||
)
|
||||
|
||||
if not product_data.title or not product_data.title.strip():
|
||||
raise MarketplaceProductValidationException("MarketplaceProduct title is required", field="title")
|
||||
raise MarketplaceProductValidationException(
|
||||
"MarketplaceProduct title is required", field="title"
|
||||
)
|
||||
|
||||
db_product = MarketplaceProduct(**product_data.model_dump())
|
||||
db.add(db_product)
|
||||
@@ -84,30 +96,47 @@ class MarketplaceProductService:
|
||||
logger.info(f"Created product {db_product.marketplace_product_id}")
|
||||
return db_product
|
||||
|
||||
except (InvalidMarketplaceProductDataException, MarketplaceProductValidationException):
|
||||
except (
|
||||
InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductValidationException,
|
||||
):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Database integrity error: {str(e)}")
|
||||
if "marketplace_product_id" in str(e).lower() or "unique" in str(e).lower():
|
||||
raise MarketplaceProductAlreadyExistsException(product_data.marketplace_product_id)
|
||||
raise MarketplaceProductAlreadyExistsException(
|
||||
product_data.marketplace_product_id
|
||||
)
|
||||
else:
|
||||
raise MarketplaceProductValidationException("Data integrity constraint violation")
|
||||
raise MarketplaceProductValidationException(
|
||||
"Data integrity constraint violation"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating product: {str(e)}")
|
||||
raise ValidationException("Failed to create product")
|
||||
|
||||
def get_product_by_id(self, db: Session, marketplace_product_id: str) -> Optional[MarketplaceProduct]:
|
||||
def get_product_by_id(
|
||||
self, db: Session, marketplace_product_id: str
|
||||
) -> Optional[MarketplaceProduct]:
|
||||
"""Get a product by its ID."""
|
||||
try:
|
||||
return db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
|
||||
return (
|
||||
db.query(MarketplaceProduct)
|
||||
.filter(
|
||||
MarketplaceProduct.marketplace_product_id == marketplace_product_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting product {marketplace_product_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_product_by_id_or_raise(self, db: Session, marketplace_product_id: str) -> MarketplaceProduct:
|
||||
def get_product_by_id_or_raise(
|
||||
self, db: Session, marketplace_product_id: str
|
||||
) -> MarketplaceProduct:
|
||||
"""
|
||||
Get a product by its ID or raise exception.
|
||||
|
||||
@@ -127,16 +156,16 @@ class MarketplaceProductService:
|
||||
return product
|
||||
|
||||
def get_products_with_filters(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
brand: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
availability: Optional[str] = None,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
brand: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
availability: Optional[str] = None,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
) -> Tuple[List[MarketplaceProduct], int]:
|
||||
"""
|
||||
Get products with filtering and pagination.
|
||||
@@ -162,13 +191,19 @@ class MarketplaceProductService:
|
||||
if brand:
|
||||
query = query.filter(MarketplaceProduct.brand.ilike(f"%{brand}%"))
|
||||
if category:
|
||||
query = query.filter(MarketplaceProduct.google_product_category.ilike(f"%{category}%"))
|
||||
query = query.filter(
|
||||
MarketplaceProduct.google_product_category.ilike(f"%{category}%")
|
||||
)
|
||||
if availability:
|
||||
query = query.filter(MarketplaceProduct.availability == availability)
|
||||
if marketplace:
|
||||
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
|
||||
query = query.filter(
|
||||
MarketplaceProduct.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
query = query.filter(
|
||||
MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%")
|
||||
)
|
||||
if search:
|
||||
# Search in title, description, marketplace, and name
|
||||
search_term = f"%{search}%"
|
||||
@@ -188,7 +223,12 @@ class MarketplaceProductService:
|
||||
logger.error(f"Error getting products with filters: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve products")
|
||||
|
||||
def update_product(self, db: Session, marketplace_product_id: str, product_update: MarketplaceProductUpdate) -> MarketplaceProduct:
|
||||
def update_product(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace_product_id: str,
|
||||
product_update: MarketplaceProductUpdate,
|
||||
) -> MarketplaceProduct:
|
||||
"""Update product with validation."""
|
||||
try:
|
||||
product = self.get_product_by_id_or_raise(db, marketplace_product_id)
|
||||
@@ -200,7 +240,9 @@ class MarketplaceProductService:
|
||||
if "gtin" in update_data and update_data["gtin"]:
|
||||
normalized_gtin = self.gtin_processor.normalize(update_data["gtin"])
|
||||
if not normalized_gtin:
|
||||
raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin")
|
||||
raise InvalidMarketplaceProductDataException(
|
||||
"Invalid GTIN format", field="gtin"
|
||||
)
|
||||
update_data["gtin"] = normalized_gtin
|
||||
|
||||
# Process price if being updated
|
||||
@@ -217,8 +259,12 @@ class MarketplaceProductService:
|
||||
raise InvalidMarketplaceProductDataException(str(e), field="price")
|
||||
|
||||
# Validate required fields if being updated
|
||||
if "title" in update_data and (not update_data["title"] or not update_data["title"].strip()):
|
||||
raise MarketplaceProductValidationException("MarketplaceProduct title cannot be empty", field="title")
|
||||
if "title" in update_data and (
|
||||
not update_data["title"] or not update_data["title"].strip()
|
||||
):
|
||||
raise MarketplaceProductValidationException(
|
||||
"MarketplaceProduct title cannot be empty", field="title"
|
||||
)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(product, key, value)
|
||||
@@ -230,7 +276,11 @@ class MarketplaceProductService:
|
||||
logger.info(f"Updated product {marketplace_product_id}")
|
||||
return product
|
||||
|
||||
except (MarketplaceProductNotFoundException, InvalidMarketplaceProductDataException, MarketplaceProductValidationException):
|
||||
except (
|
||||
MarketplaceProductNotFoundException,
|
||||
InvalidMarketplaceProductDataException,
|
||||
MarketplaceProductValidationException,
|
||||
):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
@@ -272,7 +322,9 @@ class MarketplaceProductService:
|
||||
logger.error(f"Error deleting product {marketplace_product_id}: {str(e)}")
|
||||
raise ValidationException("Failed to delete product")
|
||||
|
||||
def get_inventory_info(self, db: Session, gtin: str) -> Optional[InventorySummaryResponse]:
|
||||
def get_inventory_info(
|
||||
self, db: Session, gtin: str
|
||||
) -> Optional[InventorySummaryResponse]:
|
||||
"""
|
||||
Get inventory information for a product by GTIN.
|
||||
|
||||
@@ -290,7 +342,9 @@ class MarketplaceProductService:
|
||||
|
||||
total_quantity = sum(entry.quantity for entry in inventory_entries)
|
||||
locations = [
|
||||
InventoryLocationResponse(location=entry.location, quantity=entry.quantity)
|
||||
InventoryLocationResponse(
|
||||
location=entry.location, quantity=entry.quantity
|
||||
)
|
||||
for entry in inventory_entries
|
||||
]
|
||||
|
||||
@@ -305,13 +359,14 @@ class MarketplaceProductService:
|
||||
import csv
|
||||
from io import StringIO
|
||||
from typing import Generator, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def generate_csv_export(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generate CSV export with streaming for memory efficiency and proper CSV escaping.
|
||||
@@ -331,9 +386,18 @@ class MarketplaceProductService:
|
||||
|
||||
# Write header row
|
||||
headers = [
|
||||
"marketplace_product_id", "title", "description", "link", "image_link",
|
||||
"availability", "price", "currency", "brand", "gtin",
|
||||
"marketplace", "name"
|
||||
"marketplace_product_id",
|
||||
"title",
|
||||
"description",
|
||||
"link",
|
||||
"image_link",
|
||||
"availability",
|
||||
"price",
|
||||
"currency",
|
||||
"brand",
|
||||
"gtin",
|
||||
"marketplace",
|
||||
"name",
|
||||
]
|
||||
writer.writerow(headers)
|
||||
yield output.getvalue()
|
||||
@@ -350,9 +414,13 @@ class MarketplaceProductService:
|
||||
|
||||
# Apply marketplace filters
|
||||
if marketplace:
|
||||
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
|
||||
query = query.filter(
|
||||
MarketplaceProduct.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
query = query.filter(
|
||||
MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%")
|
||||
)
|
||||
|
||||
products = query.offset(offset).limit(batch_size).all()
|
||||
if not products:
|
||||
@@ -392,8 +460,12 @@ class MarketplaceProductService:
|
||||
"""Check if product exists by ID."""
|
||||
try:
|
||||
return (
|
||||
db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
|
||||
is not None
|
||||
db.query(MarketplaceProduct)
|
||||
.filter(
|
||||
MarketplaceProduct.marketplace_product_id == marketplace_product_id
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking if product exists: {str(e)}")
|
||||
@@ -402,18 +474,27 @@ class MarketplaceProductService:
|
||||
# Private helper methods
|
||||
def _validate_product_data(self, product_data: dict) -> None:
|
||||
"""Validate product data structure."""
|
||||
required_fields = ['marketplace_product_id', 'title']
|
||||
required_fields = ["marketplace_product_id", "title"]
|
||||
|
||||
for field in required_fields:
|
||||
if field not in product_data or not product_data[field]:
|
||||
raise MarketplaceProductValidationException(f"{field} is required", field=field)
|
||||
raise MarketplaceProductValidationException(
|
||||
f"{field} is required", field=field
|
||||
)
|
||||
|
||||
def _normalize_product_data(self, product_data: dict) -> dict:
|
||||
"""Normalize and clean product data."""
|
||||
normalized = product_data.copy()
|
||||
|
||||
# Trim whitespace from string fields
|
||||
string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'name']
|
||||
string_fields = [
|
||||
"marketplace_product_id",
|
||||
"title",
|
||||
"description",
|
||||
"brand",
|
||||
"marketplace",
|
||||
"name",
|
||||
]
|
||||
for field in string_fields:
|
||||
if field in normalized and normalized[field]:
|
||||
normalized[field] = normalized[field].strip()
|
||||
|
||||
@@ -1 +1 @@
|
||||
# File and media management services
|
||||
# File and media management services
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Application monitoring services
|
||||
# Application monitoring services
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Email/notification services
|
||||
# Email/notification services
|
||||
|
||||
@@ -9,24 +9,21 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.database.order import Order, OrderItem
|
||||
from app.exceptions import (CustomerNotFoundException,
|
||||
InsufficientInventoryException,
|
||||
OrderNotFoundException, ValidationException)
|
||||
from models.database.customer import Customer, CustomerAddress
|
||||
from models.database.order import Order, OrderItem
|
||||
from models.database.product import Product
|
||||
from models.schema.order import OrderCreate, OrderUpdate, OrderAddressCreate
|
||||
from app.exceptions import (
|
||||
OrderNotFoundException,
|
||||
ValidationException,
|
||||
InsufficientInventoryException,
|
||||
CustomerNotFoundException
|
||||
)
|
||||
from models.schema.order import OrderAddressCreate, OrderCreate, OrderUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,23 +39,27 @@ class OrderService:
|
||||
Example: ORD-1-20250110-A1B2C3
|
||||
"""
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d")
|
||||
random_suffix = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||||
random_suffix = "".join(
|
||||
random.choices(string.ascii_uppercase + string.digits, k=6)
|
||||
)
|
||||
order_number = f"ORD-{vendor_id}-{timestamp}-{random_suffix}"
|
||||
|
||||
# Ensure uniqueness
|
||||
while db.query(Order).filter(Order.order_number == order_number).first():
|
||||
random_suffix = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||||
random_suffix = "".join(
|
||||
random.choices(string.ascii_uppercase + string.digits, k=6)
|
||||
)
|
||||
order_number = f"ORD-{vendor_id}-{timestamp}-{random_suffix}"
|
||||
|
||||
return order_number
|
||||
|
||||
def _create_customer_address(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
address_data: OrderAddressCreate,
|
||||
address_type: str
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
address_data: OrderAddressCreate,
|
||||
address_type: str,
|
||||
) -> CustomerAddress:
|
||||
"""Create a customer address for order."""
|
||||
address = CustomerAddress(
|
||||
@@ -73,17 +74,14 @@ class OrderService:
|
||||
city=address_data.city,
|
||||
postal_code=address_data.postal_code,
|
||||
country=address_data.country,
|
||||
is_default=False
|
||||
is_default=False,
|
||||
)
|
||||
db.add(address)
|
||||
db.flush() # Get ID without committing
|
||||
return address
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
order_data: OrderCreate
|
||||
self, db: Session, vendor_id: int, order_data: OrderCreate
|
||||
) -> Order:
|
||||
"""
|
||||
Create a new order.
|
||||
@@ -104,12 +102,15 @@ class OrderService:
|
||||
# Validate customer exists if provided
|
||||
customer_id = order_data.customer_id
|
||||
if customer_id:
|
||||
customer = db.query(Customer).filter(
|
||||
and_(
|
||||
Customer.id == customer_id,
|
||||
Customer.vendor_id == vendor_id
|
||||
customer = (
|
||||
db.query(Customer)
|
||||
.filter(
|
||||
and_(
|
||||
Customer.id == customer_id, Customer.vendor_id == vendor_id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not customer:
|
||||
raise CustomerNotFoundException(str(customer_id))
|
||||
@@ -124,7 +125,7 @@ class OrderService:
|
||||
vendor_id=vendor_id,
|
||||
customer_id=customer_id,
|
||||
address_data=order_data.shipping_address,
|
||||
address_type="shipping"
|
||||
address_type="shipping",
|
||||
)
|
||||
|
||||
# Create billing address (use shipping if not provided)
|
||||
@@ -134,7 +135,7 @@ class OrderService:
|
||||
vendor_id=vendor_id,
|
||||
customer_id=customer_id,
|
||||
address_data=order_data.billing_address,
|
||||
address_type="billing"
|
||||
address_type="billing",
|
||||
)
|
||||
else:
|
||||
billing_address = shipping_address
|
||||
@@ -145,23 +146,29 @@ class OrderService:
|
||||
|
||||
for item_data in order_data.items:
|
||||
# Get product
|
||||
product = db.query(Product).filter(
|
||||
and_(
|
||||
Product.id == item_data.product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
and_(
|
||||
Product.id == item_data.product_id,
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not product:
|
||||
raise ValidationException(f"Product {item_data.product_id} not found")
|
||||
raise ValidationException(
|
||||
f"Product {item_data.product_id} not found"
|
||||
)
|
||||
|
||||
# Check inventory
|
||||
if product.available_inventory < item_data.quantity:
|
||||
raise InsufficientInventoryException(
|
||||
product_id=product.id,
|
||||
requested=item_data.quantity,
|
||||
available=product.available_inventory
|
||||
available=product.available_inventory,
|
||||
)
|
||||
|
||||
# Calculate item total
|
||||
@@ -172,14 +179,16 @@ class OrderService:
|
||||
item_total = unit_price * item_data.quantity
|
||||
subtotal += item_total
|
||||
|
||||
order_items_data.append({
|
||||
"product_id": product.id,
|
||||
"product_name": product.marketplace_product.title,
|
||||
"product_sku": product.product_id,
|
||||
"quantity": item_data.quantity,
|
||||
"unit_price": unit_price,
|
||||
"total_price": item_total
|
||||
})
|
||||
order_items_data.append(
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_name": product.marketplace_product.title,
|
||||
"product_sku": product.product_id,
|
||||
"quantity": item_data.quantity,
|
||||
"unit_price": unit_price,
|
||||
"total_price": item_total,
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate tax and shipping (simple implementation)
|
||||
tax_amount = 0.0 # TODO: Implement tax calculation
|
||||
@@ -205,7 +214,7 @@ class OrderService:
|
||||
shipping_address_id=shipping_address.id,
|
||||
billing_address_id=billing_address.id,
|
||||
shipping_method=order_data.shipping_method,
|
||||
customer_notes=order_data.customer_notes
|
||||
customer_notes=order_data.customer_notes,
|
||||
)
|
||||
|
||||
db.add(order)
|
||||
@@ -213,10 +222,7 @@ class OrderService:
|
||||
|
||||
# Create order items
|
||||
for item_data in order_items_data:
|
||||
order_item = OrderItem(
|
||||
order_id=order.id,
|
||||
**item_data
|
||||
)
|
||||
order_item = OrderItem(order_id=order.id, **item_data)
|
||||
db.add(order_item)
|
||||
|
||||
db.commit()
|
||||
@@ -229,7 +235,11 @@ class OrderService:
|
||||
|
||||
return order
|
||||
|
||||
except (ValidationException, InsufficientInventoryException, CustomerNotFoundException):
|
||||
except (
|
||||
ValidationException,
|
||||
InsufficientInventoryException,
|
||||
CustomerNotFoundException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -237,19 +247,13 @@ class OrderService:
|
||||
logger.error(f"Error creating order: {str(e)}")
|
||||
raise ValidationException(f"Failed to create order: {str(e)}")
|
||||
|
||||
def get_order(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
order_id: int
|
||||
) -> Order:
|
||||
def get_order(self, db: Session, vendor_id: int, order_id: int) -> Order:
|
||||
"""Get order by ID."""
|
||||
order = db.query(Order).filter(
|
||||
and_(
|
||||
Order.id == order_id,
|
||||
Order.vendor_id == vendor_id
|
||||
)
|
||||
).first()
|
||||
order = (
|
||||
db.query(Order)
|
||||
.filter(and_(Order.id == order_id, Order.vendor_id == vendor_id))
|
||||
.first()
|
||||
)
|
||||
|
||||
if not order:
|
||||
raise OrderNotFoundException(str(order_id))
|
||||
@@ -257,13 +261,13 @@ class OrderService:
|
||||
return order
|
||||
|
||||
def get_vendor_orders(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[str] = None,
|
||||
customer_id: Optional[int] = None
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[str] = None,
|
||||
customer_id: Optional[int] = None,
|
||||
) -> Tuple[List[Order], int]:
|
||||
"""
|
||||
Get orders for vendor with filtering.
|
||||
@@ -296,28 +300,20 @@ class OrderService:
|
||||
return orders, total
|
||||
|
||||
def get_customer_orders(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Tuple[List[Order], int]:
|
||||
"""Get orders for a specific customer."""
|
||||
return self.get_vendor_orders(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
customer_id=customer_id
|
||||
db=db, vendor_id=vendor_id, skip=skip, limit=limit, customer_id=customer_id
|
||||
)
|
||||
|
||||
def update_order_status(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
order_id: int,
|
||||
order_update: OrderUpdate
|
||||
self, db: Session, vendor_id: int, order_id: int, order_update: OrderUpdate
|
||||
) -> Order:
|
||||
"""
|
||||
Update order status and tracking information.
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Payment processing services
|
||||
# Payment processing services
|
||||
|
||||
@@ -14,14 +14,11 @@ from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ProductNotFoundException,
|
||||
ProductAlreadyExistsException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schema.product import ProductCreate, ProductUpdate
|
||||
from models.database.product import Product
|
||||
from app.exceptions import (ProductAlreadyExistsException,
|
||||
ProductNotFoundException, ValidationException)
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.product import Product
|
||||
from models.schema.product import ProductCreate, ProductUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,10 +42,11 @@ class ProductService:
|
||||
ProductNotFoundException: If product not found
|
||||
"""
|
||||
try:
|
||||
product = db.query(Product).filter(
|
||||
Product.id == product_id,
|
||||
Product.vendor_id == vendor_id
|
||||
).first()
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(Product.id == product_id, Product.vendor_id == vendor_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not product:
|
||||
raise ProductNotFoundException(f"Product {product_id} not found")
|
||||
@@ -62,7 +60,7 @@ class ProductService:
|
||||
raise ValidationException("Failed to retrieve product")
|
||||
|
||||
def create_product(
|
||||
self, db: Session, vendor_id: int, product_data: ProductCreate
|
||||
self, db: Session, vendor_id: int, product_data: ProductCreate
|
||||
) -> Product:
|
||||
"""
|
||||
Add a product from marketplace to vendor catalog.
|
||||
@@ -81,10 +79,14 @@ class ProductService:
|
||||
"""
|
||||
try:
|
||||
# Verify marketplace product exists and belongs to vendor
|
||||
marketplace_product = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.id == product_data.marketplace_product_id,
|
||||
MarketplaceProduct.vendor_id == vendor_id
|
||||
).first()
|
||||
marketplace_product = (
|
||||
db.query(MarketplaceProduct)
|
||||
.filter(
|
||||
MarketplaceProduct.id == product_data.marketplace_product_id,
|
||||
MarketplaceProduct.vendor_id == vendor_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not marketplace_product:
|
||||
raise ValidationException(
|
||||
@@ -92,10 +94,15 @@ class ProductService:
|
||||
)
|
||||
|
||||
# Check if already in catalog
|
||||
existing = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.marketplace_product_id == product_data.marketplace_product_id
|
||||
).first()
|
||||
existing = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.marketplace_product_id
|
||||
== product_data.marketplace_product_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing:
|
||||
raise ProductAlreadyExistsException(
|
||||
@@ -122,9 +129,7 @@ class ProductService:
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
logger.info(
|
||||
f"Added product {product.id} to vendor {vendor_id} catalog"
|
||||
)
|
||||
logger.info(f"Added product {product.id} to vendor {vendor_id} catalog")
|
||||
return product
|
||||
|
||||
except (ProductAlreadyExistsException, ValidationException):
|
||||
@@ -136,7 +141,11 @@ class ProductService:
|
||||
raise ValidationException("Failed to create product")
|
||||
|
||||
def update_product(
|
||||
self, db: Session, vendor_id: int, product_id: int, product_update: ProductUpdate
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
product_id: int,
|
||||
product_update: ProductUpdate,
|
||||
) -> Product:
|
||||
"""
|
||||
Update product in vendor catalog.
|
||||
@@ -202,13 +211,13 @@ class ProductService:
|
||||
raise ValidationException("Failed to delete product")
|
||||
|
||||
def get_vendor_products(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
is_active: Optional[bool] = None,
|
||||
is_featured: Optional[bool] = None,
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
is_active: Optional[bool] = None,
|
||||
is_featured: Optional[bool] = None,
|
||||
) -> Tuple[List[Product], int]:
|
||||
"""
|
||||
Get products in vendor catalog with filtering.
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Search and indexing services
|
||||
# Search and indexing services
|
||||
|
||||
@@ -10,25 +10,21 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
VendorNotFoundException,
|
||||
AdminOperationException,
|
||||
)
|
||||
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.product import Product
|
||||
from models.database.inventory import Inventory
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.order import Order
|
||||
from models.database.user import User
|
||||
from app.exceptions import AdminOperationException, VendorNotFoundException
|
||||
from models.database.customer import Customer
|
||||
from models.database.inventory import Inventory
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.order import Order
|
||||
from models.database.product import Product
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,63 +58,77 @@ class StatsService:
|
||||
|
||||
try:
|
||||
# Catalog statistics
|
||||
total_catalog_products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_active == True
|
||||
).count()
|
||||
total_catalog_products = (
|
||||
db.query(Product)
|
||||
.filter(Product.vendor_id == vendor_id, Product.is_active == True)
|
||||
.count()
|
||||
)
|
||||
|
||||
featured_products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_featured == True,
|
||||
Product.is_active == True
|
||||
).count()
|
||||
featured_products = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_featured == True,
|
||||
Product.is_active == True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Staging statistics
|
||||
# TODO: This is fragile - MarketplaceProduct uses vendor_name (string) not vendor_id
|
||||
# Should add vendor_id foreign key to MarketplaceProduct for robust querying
|
||||
# For now, matching by vendor name which could fail if names don't match exactly
|
||||
staging_products = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.vendor_name == vendor.name
|
||||
).count()
|
||||
staging_products = (
|
||||
db.query(MarketplaceProduct)
|
||||
.filter(MarketplaceProduct.vendor_name == vendor.name)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Inventory statistics
|
||||
total_inventory = db.query(
|
||||
func.sum(Inventory.quantity)
|
||||
).filter(
|
||||
Inventory.vendor_id == vendor_id
|
||||
).scalar() or 0
|
||||
total_inventory = (
|
||||
db.query(func.sum(Inventory.quantity))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
reserved_inventory = db.query(
|
||||
func.sum(Inventory.reserved_quantity)
|
||||
).filter(
|
||||
Inventory.vendor_id == vendor_id
|
||||
).scalar() or 0
|
||||
reserved_inventory = (
|
||||
db.query(func.sum(Inventory.reserved_quantity))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
inventory_locations = db.query(
|
||||
func.count(func.distinct(Inventory.location))
|
||||
).filter(
|
||||
Inventory.vendor_id == vendor_id
|
||||
).scalar() or 0
|
||||
inventory_locations = (
|
||||
db.query(func.count(func.distinct(Inventory.location)))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Import statistics
|
||||
total_imports = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id
|
||||
).count()
|
||||
total_imports = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.vendor_id == vendor_id)
|
||||
.count()
|
||||
)
|
||||
|
||||
successful_imports = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id,
|
||||
MarketplaceImportJob.status == "completed"
|
||||
).count()
|
||||
successful_imports = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id,
|
||||
MarketplaceImportJob.status == "completed",
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Orders
|
||||
total_orders = db.query(Order).filter(
|
||||
Order.vendor_id == vendor_id
|
||||
).count()
|
||||
total_orders = db.query(Order).filter(Order.vendor_id == vendor_id).count()
|
||||
|
||||
# Customers
|
||||
total_customers = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor_id
|
||||
).count()
|
||||
total_customers = (
|
||||
db.query(Customer).filter(Customer.vendor_id == vendor_id).count()
|
||||
)
|
||||
|
||||
return {
|
||||
"catalog": {
|
||||
@@ -138,7 +148,11 @@ class StatsService:
|
||||
"imports": {
|
||||
"total_imports": total_imports,
|
||||
"successful_imports": successful_imports,
|
||||
"success_rate": (successful_imports / total_imports * 100) if total_imports > 0 else 0,
|
||||
"success_rate": (
|
||||
(successful_imports / total_imports * 100)
|
||||
if total_imports > 0
|
||||
else 0
|
||||
),
|
||||
},
|
||||
"orders": {
|
||||
"total_orders": total_orders,
|
||||
@@ -151,16 +165,18 @@ class StatsService:
|
||||
except VendorNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve vendor statistics for vendor {vendor_id}: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to retrieve vendor statistics for vendor {vendor_id}: {str(e)}"
|
||||
)
|
||||
raise AdminOperationException(
|
||||
operation="get_vendor_stats",
|
||||
reason=f"Database query failed: {str(e)}",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id)
|
||||
target_id=str(vendor_id),
|
||||
)
|
||||
|
||||
def get_vendor_analytics(
|
||||
self, db: Session, vendor_id: int, period: str = "30d"
|
||||
self, db: Session, vendor_id: int, period: str = "30d"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get a specific vendor analytics for a time period.
|
||||
@@ -188,21 +204,28 @@ class StatsService:
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Import activity
|
||||
recent_imports = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id,
|
||||
MarketplaceImportJob.created_at >= start_date
|
||||
).count()
|
||||
recent_imports = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(
|
||||
MarketplaceImportJob.vendor_id == vendor_id,
|
||||
MarketplaceImportJob.created_at >= start_date,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Products added to catalog
|
||||
products_added = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.created_at >= start_date
|
||||
).count()
|
||||
products_added = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id == vendor_id, Product.created_at >= start_date
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Inventory changes
|
||||
inventory_entries = db.query(Inventory).filter(
|
||||
Inventory.vendor_id == vendor_id
|
||||
).count()
|
||||
inventory_entries = (
|
||||
db.query(Inventory).filter(Inventory.vendor_id == vendor_id).count()
|
||||
)
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
@@ -221,12 +244,14 @@ class StatsService:
|
||||
except VendorNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve vendor analytics for vendor {vendor_id}: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to retrieve vendor analytics for vendor {vendor_id}: {str(e)}"
|
||||
)
|
||||
raise AdminOperationException(
|
||||
operation="get_vendor_analytics",
|
||||
reason=f"Database query failed: {str(e)}",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id)
|
||||
target_id=str(vendor_id),
|
||||
)
|
||||
|
||||
def get_vendor_statistics(self, db: Session) -> dict:
|
||||
@@ -234,7 +259,9 @@ class StatsService:
|
||||
try:
|
||||
total_vendors = db.query(Vendor).count()
|
||||
active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count()
|
||||
verified_vendors = db.query(Vendor).filter(Vendor.is_verified == True).count()
|
||||
verified_vendors = (
|
||||
db.query(Vendor).filter(Vendor.is_verified == True).count()
|
||||
)
|
||||
inactive_vendors = total_vendors - active_vendors
|
||||
|
||||
return {
|
||||
@@ -242,13 +269,14 @@ class StatsService:
|
||||
"active_vendors": active_vendors,
|
||||
"inactive_vendors": inactive_vendors,
|
||||
"verified_vendors": verified_vendors,
|
||||
"verification_rate": (verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
||||
"verification_rate": (
|
||||
(verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get vendor statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_vendor_statistics",
|
||||
reason="Database query failed"
|
||||
operation="get_vendor_statistics", reason="Database query failed"
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
@@ -302,7 +330,7 @@ class StatsService:
|
||||
logger.error(f"Failed to retrieve comprehensive statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_comprehensive_stats",
|
||||
reason=f"Database query failed: {str(e)}"
|
||||
reason=f"Database query failed: {str(e)}",
|
||||
)
|
||||
|
||||
def get_marketplace_breakdown_stats(self, db: Session) -> List[Dict[str, Any]]:
|
||||
@@ -323,8 +351,12 @@ class StatsService:
|
||||
db.query(
|
||||
MarketplaceProduct.marketplace,
|
||||
func.count(MarketplaceProduct.id).label("total_products"),
|
||||
func.count(func.distinct(MarketplaceProduct.vendor_name)).label("unique_vendors"),
|
||||
func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"),
|
||||
func.count(func.distinct(MarketplaceProduct.vendor_name)).label(
|
||||
"unique_vendors"
|
||||
),
|
||||
func.count(func.distinct(MarketplaceProduct.brand)).label(
|
||||
"unique_brands"
|
||||
),
|
||||
)
|
||||
.filter(MarketplaceProduct.marketplace.isnot(None))
|
||||
.group_by(MarketplaceProduct.marketplace)
|
||||
@@ -342,10 +374,12 @@ class StatsService:
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve marketplace breakdown statistics: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to retrieve marketplace breakdown statistics: {str(e)}"
|
||||
)
|
||||
raise AdminOperationException(
|
||||
operation="get_marketplace_breakdown_stats",
|
||||
reason=f"Database query failed: {str(e)}"
|
||||
reason=f"Database query failed: {str(e)}",
|
||||
)
|
||||
|
||||
def get_user_statistics(self, db: Session) -> Dict[str, Any]:
|
||||
@@ -372,13 +406,14 @@ class StatsService:
|
||||
"active_users": active_users,
|
||||
"inactive_users": inactive_users,
|
||||
"admin_users": admin_users,
|
||||
"activation_rate": (active_users / total_users * 100) if total_users > 0 else 0
|
||||
"activation_rate": (
|
||||
(active_users / total_users * 100) if total_users > 0 else 0
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_user_statistics",
|
||||
reason="Database query failed"
|
||||
operation="get_user_statistics", reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_import_statistics(self, db: Session) -> Dict[str, Any]:
|
||||
@@ -396,18 +431,22 @@ class StatsService:
|
||||
"""
|
||||
try:
|
||||
total = db.query(MarketplaceImportJob).count()
|
||||
completed = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.status == "completed"
|
||||
).count()
|
||||
failed = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.status == "failed"
|
||||
).count()
|
||||
completed = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.status == "completed")
|
||||
.count()
|
||||
)
|
||||
failed = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.status == "failed")
|
||||
.count()
|
||||
)
|
||||
|
||||
return {
|
||||
"total_imports": total,
|
||||
"completed_imports": completed,
|
||||
"failed_imports": failed,
|
||||
"success_rate": (completed / total * 100) if total > 0 else 0
|
||||
"success_rate": (completed / total * 100) if total > 0 else 0,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get import statistics: {str(e)}")
|
||||
@@ -415,7 +454,7 @@ class StatsService:
|
||||
"total_imports": 0,
|
||||
"completed_imports": 0,
|
||||
"failed_imports": 0,
|
||||
"success_rate": 0
|
||||
"success_rate": 0,
|
||||
}
|
||||
|
||||
def get_order_statistics(self, db: Session) -> Dict[str, Any]:
|
||||
@@ -431,11 +470,7 @@ class StatsService:
|
||||
Note:
|
||||
TODO: Implement when Order model is fully available
|
||||
"""
|
||||
return {
|
||||
"total_orders": 0,
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0
|
||||
}
|
||||
return {"total_orders": 0, "pending_orders": 0, "completed_orders": 0}
|
||||
|
||||
def get_product_statistics(self, db: Session) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -450,11 +485,7 @@ class StatsService:
|
||||
Note:
|
||||
TODO: Implement when Product model is fully available
|
||||
"""
|
||||
return {
|
||||
"total_products": 0,
|
||||
"active_products": 0,
|
||||
"out_of_stock": 0
|
||||
}
|
||||
return {"total_products": 0, "active_products": 0, "out_of_stock": 0}
|
||||
|
||||
# ========================================================================
|
||||
# PRIVATE HELPER METHODS
|
||||
@@ -491,8 +522,7 @@ class StatsService:
|
||||
return (
|
||||
db.query(MarketplaceProduct.brand)
|
||||
.filter(
|
||||
MarketplaceProduct.brand.isnot(None),
|
||||
MarketplaceProduct.brand != ""
|
||||
MarketplaceProduct.brand.isnot(None), MarketplaceProduct.brand != ""
|
||||
)
|
||||
.distinct()
|
||||
.count()
|
||||
|
||||
@@ -9,17 +9,15 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ValidationException,
|
||||
UnauthorizedVendorAccessException,
|
||||
)
|
||||
from models.database.vendor import VendorUser, Role
|
||||
from app.exceptions import (UnauthorizedVendorAccessException,
|
||||
ValidationException)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Role, VendorUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,7 +26,7 @@ class TeamService:
|
||||
"""Service for team management operations."""
|
||||
|
||||
def get_team_members(
|
||||
self, db: Session, vendor_id: int, current_user: User
|
||||
self, db: Session, vendor_id: int, current_user: User
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all team members for vendor.
|
||||
@@ -42,23 +40,26 @@ class TeamService:
|
||||
List of team members
|
||||
"""
|
||||
try:
|
||||
vendor_users = db.query(VendorUser).filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.is_active == True
|
||||
).all()
|
||||
vendor_users = (
|
||||
db.query(VendorUser)
|
||||
.filter(VendorUser.vendor_id == vendor_id, VendorUser.is_active == True)
|
||||
.all()
|
||||
)
|
||||
|
||||
members = []
|
||||
for vu in vendor_users:
|
||||
members.append({
|
||||
"id": vu.user_id,
|
||||
"email": vu.user.email,
|
||||
"first_name": vu.user.first_name,
|
||||
"last_name": vu.user.last_name,
|
||||
"role": vu.role.name,
|
||||
"role_id": vu.role_id,
|
||||
"is_active": vu.is_active,
|
||||
"joined_at": vu.created_at,
|
||||
})
|
||||
members.append(
|
||||
{
|
||||
"id": vu.user_id,
|
||||
"email": vu.user.email,
|
||||
"first_name": vu.user.first_name,
|
||||
"last_name": vu.user.last_name,
|
||||
"role": vu.role.name,
|
||||
"role_id": vu.role_id,
|
||||
"is_active": vu.is_active,
|
||||
"joined_at": vu.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
return members
|
||||
|
||||
@@ -67,7 +68,7 @@ class TeamService:
|
||||
raise ValidationException("Failed to retrieve team members")
|
||||
|
||||
def invite_team_member(
|
||||
self, db: Session, vendor_id: int, invitation_data: dict, current_user: User
|
||||
self, db: Session, vendor_id: int, invitation_data: dict, current_user: User
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Invite a new team member.
|
||||
@@ -95,12 +96,12 @@ class TeamService:
|
||||
raise ValidationException("Failed to invite team member")
|
||||
|
||||
def update_team_member(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
user_id: int,
|
||||
update_data: dict,
|
||||
current_user: User
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
user_id: int,
|
||||
update_data: dict,
|
||||
current_user: User,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update team member role or status.
|
||||
@@ -116,10 +117,13 @@ class TeamService:
|
||||
Updated member info
|
||||
"""
|
||||
try:
|
||||
vendor_user = db.query(VendorUser).filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == user_id
|
||||
).first()
|
||||
vendor_user = (
|
||||
db.query(VendorUser)
|
||||
.filter(
|
||||
VendorUser.vendor_id == vendor_id, VendorUser.user_id == user_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not vendor_user:
|
||||
raise ValidationException("Team member not found")
|
||||
@@ -146,7 +150,7 @@ class TeamService:
|
||||
raise ValidationException("Failed to update team member")
|
||||
|
||||
def remove_team_member(
|
||||
self, db: Session, vendor_id: int, user_id: int, current_user: User
|
||||
self, db: Session, vendor_id: int, user_id: int, current_user: User
|
||||
) -> bool:
|
||||
"""
|
||||
Remove team member from vendor.
|
||||
@@ -161,10 +165,13 @@ class TeamService:
|
||||
True if removed
|
||||
"""
|
||||
try:
|
||||
vendor_user = db.query(VendorUser).filter(
|
||||
VendorUser.vendor_id == vendor_id,
|
||||
VendorUser.user_id == user_id
|
||||
).first()
|
||||
vendor_user = (
|
||||
db.query(VendorUser)
|
||||
.filter(
|
||||
VendorUser.vendor_id == vendor_id, VendorUser.user_id == user_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not vendor_user:
|
||||
raise ValidationException("Team member not found")
|
||||
|
||||
@@ -12,30 +12,28 @@ This module provides classes and functions for:
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from typing import List, Tuple, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
VendorNotFoundException,
|
||||
VendorDomainNotFoundException,
|
||||
VendorDomainAlreadyExistsException,
|
||||
InvalidDomainFormatException,
|
||||
ReservedDomainException,
|
||||
DomainNotVerifiedException,
|
||||
DomainVerificationFailedException,
|
||||
DomainAlreadyVerifiedException,
|
||||
MultiplePrimaryDomainsException,
|
||||
DNSVerificationException,
|
||||
MaxDomainsReachedException,
|
||||
UnauthorizedDomainAccessException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schema.vendor_domain import VendorDomainCreate, VendorDomainUpdate
|
||||
from app.exceptions import (DNSVerificationException,
|
||||
DomainAlreadyVerifiedException,
|
||||
DomainNotVerifiedException,
|
||||
DomainVerificationFailedException,
|
||||
InvalidDomainFormatException,
|
||||
MaxDomainsReachedException,
|
||||
MultiplePrimaryDomainsException,
|
||||
ReservedDomainException,
|
||||
UnauthorizedDomainAccessException,
|
||||
ValidationException,
|
||||
VendorDomainAlreadyExistsException,
|
||||
VendorDomainNotFoundException,
|
||||
VendorNotFoundException)
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.vendor_domain import VendorDomain
|
||||
from models.schema.vendor_domain import VendorDomainCreate, VendorDomainUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,13 +43,19 @@ class VendorDomainService:
|
||||
|
||||
def __init__(self):
|
||||
self.max_domains_per_vendor = 10 # Configure as needed
|
||||
self.reserved_subdomains = ['www', 'admin', 'api', 'mail', 'smtp', 'ftp', 'cpanel', 'webmail']
|
||||
self.reserved_subdomains = [
|
||||
"www",
|
||||
"admin",
|
||||
"api",
|
||||
"mail",
|
||||
"smtp",
|
||||
"ftp",
|
||||
"cpanel",
|
||||
"webmail",
|
||||
]
|
||||
|
||||
def add_domain(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
domain_data: VendorDomainCreate
|
||||
self, db: Session, vendor_id: int, domain_data: VendorDomainCreate
|
||||
) -> VendorDomain:
|
||||
"""
|
||||
Add a custom domain to vendor.
|
||||
@@ -85,12 +89,14 @@ class VendorDomainService:
|
||||
|
||||
# Check if domain already exists
|
||||
if self._domain_exists(db, normalized_domain):
|
||||
existing_domain = db.query(VendorDomain).filter(
|
||||
VendorDomain.domain == normalized_domain
|
||||
).first()
|
||||
existing_domain = (
|
||||
db.query(VendorDomain)
|
||||
.filter(VendorDomain.domain == normalized_domain)
|
||||
.first()
|
||||
)
|
||||
raise VendorDomainAlreadyExistsException(
|
||||
normalized_domain,
|
||||
existing_domain.vendor_id if existing_domain else None
|
||||
existing_domain.vendor_id if existing_domain else None,
|
||||
)
|
||||
|
||||
# If setting as primary, unset other primary domains
|
||||
@@ -104,8 +110,8 @@ class VendorDomainService:
|
||||
is_primary=domain_data.is_primary,
|
||||
verification_token=secrets.token_urlsafe(32),
|
||||
is_verified=False, # Requires DNS verification
|
||||
is_active=False, # Cannot be active until verified
|
||||
ssl_status="pending"
|
||||
is_active=False, # Cannot be active until verified
|
||||
ssl_status="pending",
|
||||
)
|
||||
|
||||
db.add(new_domain)
|
||||
@@ -120,7 +126,7 @@ class VendorDomainService:
|
||||
VendorDomainAlreadyExistsException,
|
||||
MaxDomainsReachedException,
|
||||
InvalidDomainFormatException,
|
||||
ReservedDomainException
|
||||
ReservedDomainException,
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
@@ -129,11 +135,7 @@ class VendorDomainService:
|
||||
logger.error(f"Error adding domain: {str(e)}")
|
||||
raise ValidationException("Failed to add domain")
|
||||
|
||||
def get_vendor_domains(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int
|
||||
) -> List[VendorDomain]:
|
||||
def get_vendor_domains(self, db: Session, vendor_id: int) -> List[VendorDomain]:
|
||||
"""
|
||||
Get all domains for a vendor.
|
||||
|
||||
@@ -151,12 +153,14 @@ class VendorDomainService:
|
||||
# Verify vendor exists
|
||||
self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
domains = db.query(VendorDomain).filter(
|
||||
VendorDomain.vendor_id == vendor_id
|
||||
).order_by(
|
||||
VendorDomain.is_primary.desc(),
|
||||
VendorDomain.created_at.desc()
|
||||
).all()
|
||||
domains = (
|
||||
db.query(VendorDomain)
|
||||
.filter(VendorDomain.vendor_id == vendor_id)
|
||||
.order_by(
|
||||
VendorDomain.is_primary.desc(), VendorDomain.created_at.desc()
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
return domains
|
||||
|
||||
@@ -166,11 +170,7 @@ class VendorDomainService:
|
||||
logger.error(f"Error getting vendor domains: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve domains")
|
||||
|
||||
def get_domain_by_id(
|
||||
self,
|
||||
db: Session,
|
||||
domain_id: int
|
||||
) -> VendorDomain:
|
||||
def get_domain_by_id(self, db: Session, domain_id: int) -> VendorDomain:
|
||||
"""
|
||||
Get domain by ID.
|
||||
|
||||
@@ -190,10 +190,7 @@ class VendorDomainService:
|
||||
return domain
|
||||
|
||||
def update_domain(
|
||||
self,
|
||||
db: Session,
|
||||
domain_id: int,
|
||||
domain_update: VendorDomainUpdate
|
||||
self, db: Session, domain_id: int, domain_update: VendorDomainUpdate
|
||||
) -> VendorDomain:
|
||||
"""
|
||||
Update domain settings.
|
||||
@@ -215,7 +212,9 @@ class VendorDomainService:
|
||||
|
||||
# If setting as primary, unset other primary domains
|
||||
if domain_update.is_primary:
|
||||
self._unset_primary_domains(db, domain.vendor_id, exclude_domain_id=domain_id)
|
||||
self._unset_primary_domains(
|
||||
db, domain.vendor_id, exclude_domain_id=domain_id
|
||||
)
|
||||
domain.is_primary = True
|
||||
|
||||
# If activating, check verification
|
||||
@@ -240,11 +239,7 @@ class VendorDomainService:
|
||||
logger.error(f"Error updating domain: {str(e)}")
|
||||
raise ValidationException("Failed to update domain")
|
||||
|
||||
def delete_domain(
|
||||
self,
|
||||
db: Session,
|
||||
domain_id: int
|
||||
) -> str:
|
||||
def delete_domain(self, db: Session, domain_id: int) -> str:
|
||||
"""
|
||||
Delete a custom domain.
|
||||
|
||||
@@ -277,11 +272,7 @@ class VendorDomainService:
|
||||
logger.error(f"Error deleting domain: {str(e)}")
|
||||
raise ValidationException("Failed to delete domain")
|
||||
|
||||
def verify_domain(
|
||||
self,
|
||||
db: Session,
|
||||
domain_id: int
|
||||
) -> Tuple[VendorDomain, str]:
|
||||
def verify_domain(self, db: Session, domain_id: int) -> Tuple[VendorDomain, str]:
|
||||
"""
|
||||
Verify domain ownership via DNS TXT record.
|
||||
|
||||
@@ -313,8 +304,7 @@ class VendorDomainService:
|
||||
# Query DNS TXT records
|
||||
try:
|
||||
txt_records = dns.resolver.resolve(
|
||||
f"_wizamart-verify.{domain.domain}",
|
||||
'TXT'
|
||||
f"_wizamart-verify.{domain.domain}", "TXT"
|
||||
)
|
||||
|
||||
# Check if verification token is present
|
||||
@@ -332,42 +322,33 @@ class VendorDomainService:
|
||||
|
||||
# Token not found
|
||||
raise DomainVerificationFailedException(
|
||||
domain.domain,
|
||||
"Verification token not found in DNS records"
|
||||
domain.domain, "Verification token not found in DNS records"
|
||||
)
|
||||
|
||||
except dns.resolver.NXDOMAIN:
|
||||
raise DomainVerificationFailedException(
|
||||
domain.domain,
|
||||
f"DNS record _wizamart-verify.{domain.domain} not found"
|
||||
f"DNS record _wizamart-verify.{domain.domain} not found",
|
||||
)
|
||||
except dns.resolver.NoAnswer:
|
||||
raise DomainVerificationFailedException(
|
||||
domain.domain,
|
||||
"No TXT records found for verification"
|
||||
domain.domain, "No TXT records found for verification"
|
||||
)
|
||||
except Exception as dns_error:
|
||||
raise DNSVerificationException(
|
||||
domain.domain,
|
||||
str(dns_error)
|
||||
)
|
||||
raise DNSVerificationException(domain.domain, str(dns_error))
|
||||
|
||||
except (
|
||||
VendorDomainNotFoundException,
|
||||
DomainAlreadyVerifiedException,
|
||||
DomainVerificationFailedException,
|
||||
DNSVerificationException
|
||||
DNSVerificationException,
|
||||
):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying domain: {str(e)}")
|
||||
raise ValidationException("Failed to verify domain")
|
||||
|
||||
def get_verification_instructions(
|
||||
self,
|
||||
db: Session,
|
||||
domain_id: int
|
||||
) -> dict:
|
||||
def get_verification_instructions(self, db: Session, domain_id: int) -> dict:
|
||||
"""
|
||||
Get DNS verification instructions for domain.
|
||||
|
||||
@@ -390,20 +371,20 @@ class VendorDomainService:
|
||||
"step1": "Go to your domain's DNS settings (at your domain registrar)",
|
||||
"step2": "Add a new TXT record with the following values:",
|
||||
"step3": "Wait for DNS propagation (5-15 minutes)",
|
||||
"step4": "Click 'Verify Domain' button in admin panel"
|
||||
"step4": "Click 'Verify Domain' button in admin panel",
|
||||
},
|
||||
"txt_record": {
|
||||
"type": "TXT",
|
||||
"name": "_wizamart-verify",
|
||||
"value": domain.verification_token,
|
||||
"ttl": 3600
|
||||
"ttl": 3600,
|
||||
},
|
||||
"common_registrars": {
|
||||
"Cloudflare": "https://dash.cloudflare.com",
|
||||
"GoDaddy": "https://dcc.godaddy.com/manage/dns",
|
||||
"Namecheap": "https://www.namecheap.com/myaccount/domain-list/",
|
||||
"Google Domains": "https://domains.google.com"
|
||||
}
|
||||
"Google Domains": "https://domains.google.com",
|
||||
},
|
||||
}
|
||||
|
||||
# Private helper methods
|
||||
@@ -416,36 +397,33 @@ class VendorDomainService:
|
||||
|
||||
def _check_domain_limit(self, db: Session, vendor_id: int) -> None:
|
||||
"""Check if vendor has reached maximum domain limit."""
|
||||
domain_count = db.query(VendorDomain).filter(
|
||||
VendorDomain.vendor_id == vendor_id
|
||||
).count()
|
||||
domain_count = (
|
||||
db.query(VendorDomain).filter(VendorDomain.vendor_id == vendor_id).count()
|
||||
)
|
||||
|
||||
if domain_count >= self.max_domains_per_vendor:
|
||||
raise MaxDomainsReachedException(vendor_id, self.max_domains_per_vendor)
|
||||
|
||||
def _domain_exists(self, db: Session, domain: str) -> bool:
|
||||
"""Check if domain already exists in system."""
|
||||
return db.query(VendorDomain).filter(
|
||||
VendorDomain.domain == domain
|
||||
).first() is not None
|
||||
return (
|
||||
db.query(VendorDomain).filter(VendorDomain.domain == domain).first()
|
||||
is not None
|
||||
)
|
||||
|
||||
def _validate_domain_format(self, domain: str) -> None:
|
||||
"""Validate domain format and check for reserved subdomains."""
|
||||
# Check for reserved subdomains
|
||||
first_part = domain.split('.')[0]
|
||||
first_part = domain.split(".")[0]
|
||||
if first_part in self.reserved_subdomains:
|
||||
raise ReservedDomainException(domain, first_part)
|
||||
|
||||
def _unset_primary_domains(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
exclude_domain_id: Optional[int] = None
|
||||
self, db: Session, vendor_id: int, exclude_domain_id: Optional[int] = None
|
||||
) -> None:
|
||||
"""Unset all primary domains for vendor."""
|
||||
query = db.query(VendorDomain).filter(
|
||||
VendorDomain.vendor_id == vendor_id,
|
||||
VendorDomain.is_primary == True
|
||||
VendorDomain.vendor_id == vendor_id, VendorDomain.is_primary == True
|
||||
)
|
||||
|
||||
if exclude_domain_id:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user