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:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
# Platform monitoring and alerts
# Platform monitoring and alerts

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
# Health checks
# Health checks

View File

@@ -1 +1 @@
# File upload handling
# File upload handling

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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