refactor: enforce strict architecture rules and add Pydantic response models
- Update architecture rules to be stricter (API-003 now blocks ALL exception raising in endpoints, not just HTTPException) - Update get_current_vendor_api dependency to guarantee token_vendor_id presence - Remove redundant _get_vendor_from_token helpers from all vendor API files - Move vendor access validation to service layer methods - Add Pydantic response models for media, notification, and payment endpoints - Add get_active_vendor_by_code service method for public vendor lookup - Add get_import_job_for_vendor service method with vendor validation - Update validation script to detect exception raising patterns in endpoints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -75,17 +75,42 @@ api_endpoint_rules:
|
|||||||
# NOTE: db.commit() is intentionally NOT listed - it's allowed for transaction control
|
# NOTE: db.commit() is intentionally NOT listed - it's allowed for transaction control
|
||||||
|
|
||||||
- id: "API-003"
|
- id: "API-003"
|
||||||
name: "Endpoint must NOT raise HTTPException directly"
|
name: "Endpoint must NOT raise ANY exceptions directly"
|
||||||
severity: "error"
|
severity: "error"
|
||||||
description: |
|
description: |
|
||||||
API endpoints should NOT raise HTTPException directly. Instead, let domain
|
API endpoints should NOT raise exceptions directly. Endpoints are a thin
|
||||||
exceptions bubble up to the global exception handler which converts them
|
orchestration layer that:
|
||||||
to appropriate HTTP responses. This ensures consistent error formatting
|
1. Accepts request parameters (validated by Pydantic)
|
||||||
and centralized error handling.
|
2. Calls dependencies for authentication/authorization (deps.py raises exceptions)
|
||||||
|
3. Calls services for business logic (services raise domain exceptions)
|
||||||
|
4. Returns response (formatted by Pydantic)
|
||||||
|
|
||||||
|
Exception raising belongs in:
|
||||||
|
- Dependencies (app/api/deps.py) - authentication/authorization validation
|
||||||
|
- Services (app/services/) - business logic validation
|
||||||
|
|
||||||
|
The global exception handler catches all WizamartException subclasses and
|
||||||
|
converts them to appropriate HTTP responses.
|
||||||
|
|
||||||
|
WRONG (endpoint raises exception):
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
||||||
|
if not hasattr(current_user, "token_vendor_id"): # ❌ Redundant check
|
||||||
|
raise InvalidTokenException("...") # ❌ Don't raise here
|
||||||
|
return order_service.get_orders(db, current_user.token_vendor_id)
|
||||||
|
|
||||||
|
RIGHT (dependency guarantees, endpoint trusts):
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
||||||
|
# Dependency guarantees token_vendor_id is present
|
||||||
|
return order_service.get_orders(db, current_user.token_vendor_id)
|
||||||
pattern:
|
pattern:
|
||||||
file_pattern: "app/api/v1/**/*.py"
|
file_pattern: "app/api/v1/**/*.py"
|
||||||
anti_patterns:
|
anti_patterns:
|
||||||
- "raise HTTPException"
|
- "raise HTTPException"
|
||||||
|
- "raise InvalidTokenException"
|
||||||
|
- "raise InsufficientPermissionsException"
|
||||||
|
- "if not hasattr\\(current_user.*token_vendor"
|
||||||
exceptions:
|
exceptions:
|
||||||
- "app/exceptions/handler.py" # Handler is allowed to use HTTPException
|
- "app/exceptions/handler.py" # Handler is allowed to use HTTPException
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,9 @@ def get_current_vendor_api(
|
|||||||
Get current vendor user from Authorization header ONLY.
|
Get current vendor user from Authorization header ONLY.
|
||||||
|
|
||||||
Used for vendor API endpoints that should not accept cookies.
|
Used for vendor API endpoints that should not accept cookies.
|
||||||
Validates that user still has access to the vendor specified in the token.
|
Validates that:
|
||||||
|
1. Token contains vendor context (token_vendor_id)
|
||||||
|
2. User still has access to the vendor specified in the token
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
credentials: Bearer token from Authorization header
|
credentials: Bearer token from Authorization header
|
||||||
@@ -285,7 +287,7 @@ def get_current_vendor_api(
|
|||||||
User: Authenticated vendor user (with token_vendor_id, token_vendor_code, token_vendor_role)
|
User: Authenticated vendor user (with token_vendor_id, token_vendor_code, token_vendor_role)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidTokenException: If no token or invalid token
|
InvalidTokenException: If no token, invalid token, or missing vendor context
|
||||||
InsufficientPermissionsException: If user is not vendor or lost access to vendor
|
InsufficientPermissionsException: If user is not vendor or lost access to vendor
|
||||||
"""
|
"""
|
||||||
if not credentials:
|
if not credentials:
|
||||||
@@ -302,23 +304,25 @@ def get_current_vendor_api(
|
|||||||
logger.warning(f"Non-vendor user {user.username} attempted vendor API")
|
logger.warning(f"Non-vendor user {user.username} attempted vendor API")
|
||||||
raise InsufficientPermissionsException("Vendor privileges required")
|
raise InsufficientPermissionsException("Vendor privileges required")
|
||||||
|
|
||||||
# Validate vendor access if token is vendor-scoped
|
# Require vendor context in token
|
||||||
if hasattr(user, "token_vendor_id"):
|
if not hasattr(user, "token_vendor_id"):
|
||||||
vendor_id = user.token_vendor_id
|
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||||
|
|
||||||
# Verify user still has access to this vendor
|
vendor_id = user.token_vendor_id
|
||||||
if not user.is_member_of(vendor_id):
|
|
||||||
logger.warning(
|
|
||||||
f"User {user.username} lost access to vendor_id={vendor_id}"
|
|
||||||
)
|
|
||||||
raise InsufficientPermissionsException(
|
|
||||||
"Access to vendor has been revoked. Please login again."
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(
|
# Verify user still has access to this vendor
|
||||||
f"Vendor API access: user={user.username}, vendor_id={vendor_id}, "
|
if not user.is_member_of(vendor_id):
|
||||||
f"vendor_code={getattr(user, 'token_vendor_code', 'N/A')}"
|
logger.warning(
|
||||||
|
f"User {user.username} lost access to vendor_id={vendor_id}"
|
||||||
)
|
)
|
||||||
|
raise InsufficientPermissionsException(
|
||||||
|
"Access to vendor has been revoked. Please login again."
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Vendor API access: user={user.username}, vendor_id={vendor_id}, "
|
||||||
|
f"vendor_code={getattr(user, 'token_vendor_code', 'N/A')}"
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ from models.schema.admin import (
|
|||||||
PlatformAlertCreate,
|
PlatformAlertCreate,
|
||||||
PlatformAlertListResponse,
|
PlatformAlertListResponse,
|
||||||
PlatformAlertResolve,
|
PlatformAlertResolve,
|
||||||
PlatformAlertResponse,
|
)
|
||||||
|
from models.schema.notification import (
|
||||||
|
AlertStatisticsResponse,
|
||||||
|
MessageResponse,
|
||||||
|
UnreadCountResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/notifications")
|
router = APIRouter(prefix="/notifications")
|
||||||
@@ -49,17 +53,17 @@ def get_notifications(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unread-count")
|
@router.get("/unread-count", response_model=UnreadCountResponse)
|
||||||
def get_unread_count(
|
def get_unread_count(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Get count of unread notifications."""
|
"""Get count of unread notifications."""
|
||||||
# TODO: Implement
|
# TODO: Implement
|
||||||
return {"unread_count": 0}
|
return UnreadCountResponse(unread_count=0)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{notification_id}/read")
|
@router.put("/{notification_id}/read", response_model=MessageResponse)
|
||||||
def mark_as_read(
|
def mark_as_read(
|
||||||
notification_id: int,
|
notification_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -67,17 +71,17 @@ def mark_as_read(
|
|||||||
):
|
):
|
||||||
"""Mark notification as read."""
|
"""Mark notification as read."""
|
||||||
# TODO: Implement
|
# TODO: Implement
|
||||||
return {"message": "Notification marked as read"}
|
return MessageResponse(message="Notification marked as read")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/mark-all-read")
|
@router.put("/mark-all-read", response_model=MessageResponse)
|
||||||
def mark_all_as_read(
|
def mark_all_as_read(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Mark all notifications as read."""
|
"""Mark all notifications as read."""
|
||||||
# TODO: Implement
|
# TODO: Implement
|
||||||
return {"message": "All notifications marked as read"}
|
return MessageResponse(message="All notifications marked as read")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -101,19 +105,19 @@ def get_platform_alerts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/alerts", response_model=PlatformAlertResponse)
|
@router.post("/alerts", response_model=MessageResponse)
|
||||||
def create_platform_alert(
|
def create_platform_alert(
|
||||||
alert_data: PlatformAlertCreate,
|
alert_data: PlatformAlertCreate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Create new platform alert (manual)."""
|
"""Create new platform alert (manual)."""
|
||||||
# TODO: Implement
|
# TODO: Implement - return PlatformAlertResponse when service is ready
|
||||||
logger.info(f"Admin {current_admin.username} created alert: {alert_data.title}")
|
logger.info(f"Admin {current_admin.username} created alert: {alert_data.title}")
|
||||||
return {}
|
return MessageResponse(message="Platform alert creation coming soon")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/alerts/{alert_id}/resolve")
|
@router.put("/alerts/{alert_id}/resolve", response_model=MessageResponse)
|
||||||
def resolve_platform_alert(
|
def resolve_platform_alert(
|
||||||
alert_id: int,
|
alert_id: int,
|
||||||
resolve_data: PlatformAlertResolve,
|
resolve_data: PlatformAlertResolve,
|
||||||
@@ -123,19 +127,19 @@ def resolve_platform_alert(
|
|||||||
"""Resolve platform alert."""
|
"""Resolve platform alert."""
|
||||||
# TODO: Implement
|
# TODO: Implement
|
||||||
logger.info(f"Admin {current_admin.username} resolved alert {alert_id}")
|
logger.info(f"Admin {current_admin.username} resolved alert {alert_id}")
|
||||||
return {"message": "Alert resolved successfully"}
|
return MessageResponse(message="Alert resolved successfully")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/alerts/stats")
|
@router.get("/alerts/stats", response_model=AlertStatisticsResponse)
|
||||||
def get_alert_statistics(
|
def get_alert_statistics(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Get alert statistics for dashboard."""
|
"""Get alert statistics for dashboard."""
|
||||||
# TODO: Implement
|
# TODO: Implement
|
||||||
return {
|
return AlertStatisticsResponse(
|
||||||
"total_alerts": 0,
|
total_alerts=0,
|
||||||
"active_alerts": 0,
|
active_alerts=0,
|
||||||
"critical_alerts": 0,
|
critical_alerts=0,
|
||||||
"resolved_today": 0,
|
resolved_today=0,
|
||||||
}
|
)
|
||||||
|
|||||||
14
app/api/v1/vendor/analytics.py
vendored
14
app/api/v1/vendor/analytics.py
vendored
@@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Vendor analytics and reporting endpoints.
|
Vendor analytics and reporting endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
|
||||||
@@ -20,13 +20,6 @@ router = APIRouter(prefix="/analytics")
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_id_from_token(current_user: User) -> int:
|
|
||||||
"""Helper to get vendor_id from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return current_user.token_vendor_id
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def get_vendor_analytics(
|
def get_vendor_analytics(
|
||||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||||
@@ -34,5 +27,4 @@ def get_vendor_analytics(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get vendor analytics data for specified time period."""
|
"""Get vendor analytics data for specified time period."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return stats_service.get_vendor_analytics(db, current_user.token_vendor_id, period)
|
||||||
return stats_service.get_vendor_analytics(db, vendor_id, period)
|
|
||||||
|
|||||||
24
app/api/v1/vendor/customers.py
vendored
24
app/api/v1/vendor/customers.py
vendored
@@ -1,9 +1,9 @@
|
|||||||
# Vendor customer management
|
|
||||||
# app/api/v1/vendor/customers.py
|
# app/api/v1/vendor/customers.py
|
||||||
"""
|
"""
|
||||||
Vendor customer management endpoints.
|
Vendor customer management endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
|
||||||
@@ -21,13 +20,6 @@ router = APIRouter(prefix="/customers")
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def get_vendor_customers(
|
def get_vendor_customers(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
@@ -46,7 +38,7 @@ def get_vendor_customers(
|
|||||||
- Support filtering by active status
|
- Support filtering by active status
|
||||||
- Return paginated results
|
- Return paginated results
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return {
|
||||||
"customers": [],
|
"customers": [],
|
||||||
"total": 0,
|
"total": 0,
|
||||||
@@ -71,7 +63,7 @@ def get_customer_details(
|
|||||||
- Include order history
|
- Include order history
|
||||||
- Include total spent, etc.
|
- Include total spent, etc.
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Customer details coming in Slice 4"}
|
return {"message": "Customer details coming in Slice 4"}
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +81,7 @@ def get_customer_orders(
|
|||||||
- Filter by vendor_id
|
- Filter by vendor_id
|
||||||
- Return order details
|
- Return order details
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"orders": [], "message": "Customer orders coming in Slice 5"}
|
return {"orders": [], "message": "Customer orders coming in Slice 5"}
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +100,7 @@ def update_customer(
|
|||||||
- Verify customer belongs to vendor
|
- Verify customer belongs to vendor
|
||||||
- Update customer preferences
|
- Update customer preferences
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Customer update coming in Slice 4"}
|
return {"message": "Customer update coming in Slice 4"}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +118,7 @@ def toggle_customer_status(
|
|||||||
- Verify customer belongs to vendor
|
- Verify customer belongs to vendor
|
||||||
- Log the change
|
- Log the change
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Customer status toggle coming in Slice 4"}
|
return {"message": "Customer status toggle coming in Slice 4"}
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +137,7 @@ def get_customer_statistics(
|
|||||||
- Average order value
|
- Average order value
|
||||||
- Last order date
|
- Last order date
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return {
|
||||||
"total_orders": 0,
|
"total_orders": 0,
|
||||||
"total_spent": 0.0,
|
"total_spent": 0.0,
|
||||||
|
|||||||
9
app/api/v1/vendor/dashboard.py
vendored
9
app/api/v1/vendor/dashboard.py
vendored
@@ -1,6 +1,9 @@
|
|||||||
# app/api/v1/vendor/dashboard.py
|
# app/api/v1/vendor/dashboard.py
|
||||||
"""
|
"""
|
||||||
Vendor dashboard and statistics endpoints.
|
Vendor dashboard and statistics endpoints.
|
||||||
|
|
||||||
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -10,7 +13,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException, VendorNotActiveException
|
from app.exceptions import VendorNotActiveException
|
||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
@@ -37,10 +40,6 @@ def get_vendor_dashboard_stats(
|
|||||||
Vendor is determined from the JWT token (vendor_id claim).
|
Vendor is determined from the JWT token (vendor_id claim).
|
||||||
Requires Authorization header (API endpoint).
|
Requires Authorization header (API endpoint).
|
||||||
"""
|
"""
|
||||||
# Get vendor ID from token (set by get_current_vendor_api)
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
vendor_id = current_user.token_vendor_id
|
||||||
|
|
||||||
# Get vendor object (raises VendorNotFoundException if not found)
|
# Get vendor object (raises VendorNotFoundException if not found)
|
||||||
|
|||||||
36
app/api/v1/vendor/info.py
vendored
36
app/api/v1/vendor/info.py
vendored
@@ -10,48 +10,16 @@ This module provides:
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path
|
from fastapi import APIRouter, Depends, Path
|
||||||
from sqlalchemy import func
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import VendorNotFoundException
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.vendor import Vendor
|
|
||||||
from models.schema.vendor import VendorDetailResponse
|
from models.schema.vendor import VendorDetailResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_by_code(db: Session, vendor_code: str) -> Vendor:
|
|
||||||
"""
|
|
||||||
Helper to get active vendor by vendor_code.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session
|
|
||||||
vendor_code: Vendor code (case-insensitive)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Vendor object
|
|
||||||
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not vendor:
|
|
||||||
logger.warning(f"Vendor not found or inactive: {vendor_code}")
|
|
||||||
raise VendorNotFoundException(vendor_code, identifier_type="code")
|
|
||||||
|
|
||||||
return vendor
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{vendor_code}", response_model=VendorDetailResponse)
|
@router.get("/{vendor_code}", response_model=VendorDetailResponse)
|
||||||
def get_vendor_info(
|
def get_vendor_info(
|
||||||
vendor_code: str = Path(..., description="Vendor code"),
|
vendor_code: str = Path(..., description="Vendor code"),
|
||||||
@@ -81,7 +49,7 @@ def get_vendor_info(
|
|||||||
"""
|
"""
|
||||||
logger.info(f"Public vendor info request: {vendor_code}")
|
logger.info(f"Public vendor info request: {vendor_code}")
|
||||||
|
|
||||||
vendor = _get_vendor_by_code(db, vendor_code)
|
vendor = vendor_service.get_active_vendor_by_code(db, vendor_code)
|
||||||
|
|
||||||
logger.info(f"Vendor info retrieved: {vendor.name} ({vendor.vendor_code})")
|
logger.info(f"Vendor info retrieved: {vendor.name} ({vendor.vendor_code})")
|
||||||
|
|
||||||
|
|||||||
38
app/api/v1/vendor/inventory.py
vendored
38
app/api/v1/vendor/inventory.py
vendored
@@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Vendor inventory management endpoints.
|
Vendor inventory management endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.inventory_service import inventory_service
|
from app.services.inventory_service import inventory_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.inventory import (
|
from models.schema.inventory import (
|
||||||
@@ -28,13 +28,6 @@ router = APIRouter()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_id_from_token(current_user: User) -> int:
|
|
||||||
"""Helper to get vendor_id from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return current_user.token_vendor_id
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/inventory/set", response_model=InventoryResponse)
|
@router.post("/inventory/set", response_model=InventoryResponse)
|
||||||
def set_inventory(
|
def set_inventory(
|
||||||
inventory: InventoryCreate,
|
inventory: InventoryCreate,
|
||||||
@@ -42,8 +35,7 @@ def set_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Set exact inventory quantity (replaces existing)."""
|
"""Set exact inventory quantity (replaces existing)."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.set_inventory(db, current_user.token_vendor_id, inventory)
|
||||||
return inventory_service.set_inventory(db, vendor_id, inventory)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/inventory/adjust", response_model=InventoryResponse)
|
@router.post("/inventory/adjust", response_model=InventoryResponse)
|
||||||
@@ -53,8 +45,7 @@ def adjust_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Adjust inventory (positive to add, negative to remove)."""
|
"""Adjust inventory (positive to add, negative to remove)."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.adjust_inventory(db, current_user.token_vendor_id, adjustment)
|
||||||
return inventory_service.adjust_inventory(db, vendor_id, adjustment)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/inventory/reserve", response_model=InventoryResponse)
|
@router.post("/inventory/reserve", response_model=InventoryResponse)
|
||||||
@@ -64,8 +55,7 @@ def reserve_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Reserve inventory for an order."""
|
"""Reserve inventory for an order."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.reserve_inventory(db, current_user.token_vendor_id, reservation)
|
||||||
return inventory_service.reserve_inventory(db, vendor_id, reservation)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/inventory/release", response_model=InventoryResponse)
|
@router.post("/inventory/release", response_model=InventoryResponse)
|
||||||
@@ -75,8 +65,7 @@ def release_reservation(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Release reserved inventory (cancel order)."""
|
"""Release reserved inventory (cancel order)."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.release_reservation(db, current_user.token_vendor_id, reservation)
|
||||||
return inventory_service.release_reservation(db, vendor_id, reservation)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/inventory/fulfill", response_model=InventoryResponse)
|
@router.post("/inventory/fulfill", response_model=InventoryResponse)
|
||||||
@@ -86,8 +75,7 @@ def fulfill_reservation(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Fulfill reservation (complete order, remove from stock)."""
|
"""Fulfill reservation (complete order, remove from stock)."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.fulfill_reservation(db, current_user.token_vendor_id, reservation)
|
||||||
return inventory_service.fulfill_reservation(db, vendor_id, reservation)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/inventory/product/{product_id}", response_model=ProductInventorySummary)
|
@router.get("/inventory/product/{product_id}", response_model=ProductInventorySummary)
|
||||||
@@ -97,8 +85,7 @@ def get_product_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get inventory summary for a product."""
|
"""Get inventory summary for a product."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
return inventory_service.get_product_inventory(db, current_user.token_vendor_id, product_id)
|
||||||
return inventory_service.get_product_inventory(db, vendor_id, product_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/inventory", response_model=InventoryListResponse)
|
@router.get("/inventory", response_model=InventoryListResponse)
|
||||||
@@ -111,9 +98,8 @@ def get_vendor_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get all inventory for vendor."""
|
"""Get all inventory for vendor."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
|
||||||
inventories = inventory_service.get_vendor_inventory(
|
inventories = inventory_service.get_vendor_inventory(
|
||||||
db, vendor_id, skip, limit, location, low_stock
|
db, current_user.token_vendor_id, skip, limit, location, low_stock
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get total count
|
# Get total count
|
||||||
@@ -132,9 +118,8 @@ def update_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update inventory entry."""
|
"""Update inventory entry."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
|
||||||
return inventory_service.update_inventory(
|
return inventory_service.update_inventory(
|
||||||
db, vendor_id, inventory_id, inventory_update
|
db, current_user.token_vendor_id, inventory_id, inventory_update
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -145,6 +130,5 @@ def delete_inventory(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Delete inventory entry."""
|
"""Delete inventory entry."""
|
||||||
vendor_id = _get_vendor_id_from_token(current_user)
|
inventory_service.delete_inventory(db, current_user.token_vendor_id, inventory_id)
|
||||||
inventory_service.delete_inventory(db, vendor_id, inventory_id)
|
|
||||||
return {"message": "Inventory deleted successfully"}
|
return {"message": "Inventory deleted successfully"}
|
||||||
|
|||||||
26
app/api/v1/vendor/marketplace.py
vendored
26
app/api/v1/vendor/marketplace.py
vendored
@@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Marketplace import endpoints for vendors.
|
Marketplace import endpoints for vendors.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException, UnauthorizedVendorAccessException
|
|
||||||
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.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from app.tasks.background_tasks import process_marketplace_import
|
from app.tasks.background_tasks import process_marketplace_import
|
||||||
@@ -27,13 +27,6 @@ router = APIRouter(prefix="/marketplace")
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/import", response_model=MarketplaceImportJobResponse)
|
@router.post("/import", response_model=MarketplaceImportJobResponse)
|
||||||
@rate_limit(max_requests=10, window_seconds=3600)
|
@rate_limit(max_requests=10, window_seconds=3600)
|
||||||
async def import_products_from_marketplace(
|
async def import_products_from_marketplace(
|
||||||
@@ -43,7 +36,7 @@ async def import_products_from_marketplace(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||||
vendor = _get_vendor_from_token(current_user, db)
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Starting marketplace import: {request.marketplace} for vendor {vendor.vendor_code} "
|
f"Starting marketplace import: {request.marketplace} for vendor {vendor.vendor_code} "
|
||||||
@@ -90,13 +83,10 @@ def get_marketplace_import_status(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get status of marketplace import job (Protected)."""
|
"""Get status of marketplace import job (Protected)."""
|
||||||
vendor = _get_vendor_from_token(current_user, db)
|
# Service validates that job belongs to vendor and raises UnauthorizedVendorAccessException if not
|
||||||
|
job = marketplace_import_job_service.get_import_job_for_vendor(
|
||||||
job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user)
|
db, job_id, current_user.token_vendor_id
|
||||||
|
)
|
||||||
# Verify job belongs to current vendor
|
|
||||||
if job.vendor_id != vendor.id:
|
|
||||||
raise UnauthorizedVendorAccessException(vendor.vendor_code, current_user.id)
|
|
||||||
|
|
||||||
return marketplace_import_job_service.convert_to_response_model(job)
|
return marketplace_import_job_service.convert_to_response_model(job)
|
||||||
|
|
||||||
@@ -110,7 +100,7 @@ def get_marketplace_import_jobs(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get marketplace import jobs for current vendor (Protected)."""
|
"""Get marketplace import jobs for current vendor (Protected)."""
|
||||||
vendor = _get_vendor_from_token(current_user, db)
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||||
|
|
||||||
jobs = marketplace_import_job_service.get_import_jobs(
|
jobs = marketplace_import_job_service.get_import_jobs(
|
||||||
db=db,
|
db=db,
|
||||||
|
|||||||
107
app/api/v1/vendor/media.py
vendored
107
app/api/v1/vendor/media.py
vendored
@@ -1,9 +1,9 @@
|
|||||||
# File and media management
|
|
||||||
# app/api/v1/vendor/media.py
|
# app/api/v1/vendor/media.py
|
||||||
"""
|
"""
|
||||||
Vendor media and file management endpoints.
|
Vendor media and file management endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,22 +13,23 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.media import (
|
||||||
|
MediaDetailResponse,
|
||||||
|
MediaListResponse,
|
||||||
|
MediaMetadataUpdate,
|
||||||
|
MediaUploadResponse,
|
||||||
|
MediaUsageResponse,
|
||||||
|
MultipleUploadResponse,
|
||||||
|
OptimizationResultResponse,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/media")
|
router = APIRouter(prefix="/media")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
@router.get("", response_model=MediaListResponse)
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def get_media_library(
|
def get_media_library(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
@@ -47,17 +48,17 @@ def get_media_library(
|
|||||||
- Support pagination
|
- Support pagination
|
||||||
- Return file URLs, sizes, metadata
|
- Return file URLs, sizes, metadata
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return MediaListResponse(
|
||||||
"media": [],
|
media=[],
|
||||||
"total": 0,
|
total=0,
|
||||||
"skip": skip,
|
skip=skip,
|
||||||
"limit": limit,
|
limit=limit,
|
||||||
"message": "Media library coming in Slice 3",
|
message="Media library coming in Slice 3",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload")
|
@router.post("/upload", response_model=MediaUploadResponse)
|
||||||
async def upload_media(
|
async def upload_media(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
folder: str | None = Query(None, description="products, general, etc."),
|
folder: str | None = Query(None, description="products, general, etc."),
|
||||||
@@ -75,15 +76,15 @@ async def upload_media(
|
|||||||
- Save metadata to database
|
- Save metadata to database
|
||||||
- Return file URL
|
- Return file URL
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return MediaUploadResponse(
|
||||||
"file_url": None,
|
file_url=None,
|
||||||
"thumbnail_url": None,
|
thumbnail_url=None,
|
||||||
"message": "Media upload coming in Slice 3",
|
message="Media upload coming in Slice 3",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload/multiple")
|
@router.post("/upload/multiple", response_model=MultipleUploadResponse)
|
||||||
async def upload_multiple_media(
|
async def upload_multiple_media(
|
||||||
files: list[UploadFile] = File(...),
|
files: list[UploadFile] = File(...),
|
||||||
folder: str | None = Query(None),
|
folder: str | None = Query(None),
|
||||||
@@ -99,15 +100,15 @@ async def upload_multiple_media(
|
|||||||
- Return list of uploaded file URLs
|
- Return list of uploaded file URLs
|
||||||
- Handle errors gracefully
|
- Handle errors gracefully
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return MultipleUploadResponse(
|
||||||
"uploaded_files": [],
|
uploaded_files=[],
|
||||||
"failed_files": [],
|
failed_files=[],
|
||||||
"message": "Multiple upload coming in Slice 3",
|
message="Multiple upload coming in Slice 3",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{media_id}")
|
@router.get("/{media_id}", response_model=MediaDetailResponse)
|
||||||
def get_media_details(
|
def get_media_details(
|
||||||
media_id: int,
|
media_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -121,14 +122,14 @@ def get_media_details(
|
|||||||
- Return file URL
|
- Return file URL
|
||||||
- Return usage information (which products use this file)
|
- Return usage information (which products use this file)
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Media details coming in Slice 3"}
|
return MediaDetailResponse(message="Media details coming in Slice 3")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{media_id}")
|
@router.put("/{media_id}", response_model=MediaDetailResponse)
|
||||||
def update_media_metadata(
|
def update_media_metadata(
|
||||||
media_id: int,
|
media_id: int,
|
||||||
metadata: dict,
|
metadata: MediaMetadataUpdate,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -141,11 +142,11 @@ def update_media_metadata(
|
|||||||
- Update tags/categories
|
- Update tags/categories
|
||||||
- Update description
|
- Update description
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Media update coming in Slice 3"}
|
return MediaDetailResponse(message="Media update coming in Slice 3")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{media_id}")
|
@router.delete("/{media_id}", response_model=MediaDetailResponse)
|
||||||
def delete_media(
|
def delete_media(
|
||||||
media_id: int,
|
media_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -161,11 +162,11 @@ def delete_media(
|
|||||||
- Delete database record
|
- Delete database record
|
||||||
- Return success/error
|
- Return success/error
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Media deletion coming in Slice 3"}
|
return MediaDetailResponse(message="Media deletion coming in Slice 3")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{media_id}/usage")
|
@router.get("/{media_id}/usage", response_model=MediaUsageResponse)
|
||||||
def get_media_usage(
|
def get_media_usage(
|
||||||
media_id: int,
|
media_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -179,15 +180,15 @@ def get_media_usage(
|
|||||||
- Check other entities using this media
|
- Check other entities using this media
|
||||||
- Return list of usage
|
- Return list of usage
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return MediaUsageResponse(
|
||||||
"products": [],
|
products=[],
|
||||||
"other_usage": [],
|
other_usage=[],
|
||||||
"message": "Media usage tracking coming in Slice 3",
|
message="Media usage tracking coming in Slice 3",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/optimize/{media_id}")
|
@router.post("/optimize/{media_id}", response_model=OptimizationResultResponse)
|
||||||
def optimize_media(
|
def optimize_media(
|
||||||
media_id: int,
|
media_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -202,5 +203,5 @@ def optimize_media(
|
|||||||
- Keep original
|
- Keep original
|
||||||
- Update database with new versions
|
- Update database with new versions
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Media optimization coming in Slice 3"}
|
return OptimizationResultResponse(message="Media optimization coming in Slice 3")
|
||||||
|
|||||||
110
app/api/v1/vendor/notifications.py
vendored
110
app/api/v1/vendor/notifications.py
vendored
@@ -1,9 +1,9 @@
|
|||||||
# Notification management
|
|
||||||
# app/api/v1/vendor/notifications.py
|
# app/api/v1/vendor/notifications.py
|
||||||
"""
|
"""
|
||||||
Vendor notification management endpoints.
|
Vendor notification management endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,22 +13,24 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.notification import (
|
||||||
|
MessageResponse,
|
||||||
|
NotificationListResponse,
|
||||||
|
NotificationSettingsResponse,
|
||||||
|
NotificationSettingsUpdate,
|
||||||
|
NotificationTemplateListResponse,
|
||||||
|
NotificationTemplateUpdate,
|
||||||
|
TestNotificationRequest,
|
||||||
|
UnreadCountResponse,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/notifications")
|
router = APIRouter(prefix="/notifications")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
@router.get("", response_model=NotificationListResponse)
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def get_notifications(
|
def get_notifications(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(50, ge=1, le=100),
|
limit: int = Query(50, ge=1, le=100),
|
||||||
@@ -45,16 +47,16 @@ def get_notifications(
|
|||||||
- Support pagination
|
- Support pagination
|
||||||
- Return notification details
|
- Return notification details
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return NotificationListResponse(
|
||||||
"notifications": [],
|
notifications=[],
|
||||||
"total": 0,
|
total=0,
|
||||||
"unread_count": 0,
|
unread_count=0,
|
||||||
"message": "Notifications coming in Slice 5",
|
message="Notifications coming in Slice 5",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unread-count")
|
@router.get("/unread-count", response_model=UnreadCountResponse)
|
||||||
def get_unread_count(
|
def get_unread_count(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -66,11 +68,11 @@ def get_unread_count(
|
|||||||
- Count unread notifications for vendor
|
- Count unread notifications for vendor
|
||||||
- Used for notification badge
|
- Used for notification badge
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"unread_count": 0, "message": "Unread count coming in Slice 5"}
|
return UnreadCountResponse(unread_count=0, message="Unread count coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{notification_id}/read")
|
@router.put("/{notification_id}/read", response_model=MessageResponse)
|
||||||
def mark_as_read(
|
def mark_as_read(
|
||||||
notification_id: int,
|
notification_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -83,11 +85,11 @@ def mark_as_read(
|
|||||||
- Mark single notification as read
|
- Mark single notification as read
|
||||||
- Update read timestamp
|
- Update read timestamp
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Mark as read coming in Slice 5"}
|
return MessageResponse(message="Mark as read coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/mark-all-read")
|
@router.put("/mark-all-read", response_model=MessageResponse)
|
||||||
def mark_all_as_read(
|
def mark_all_as_read(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -99,11 +101,11 @@ def mark_all_as_read(
|
|||||||
- Mark all vendor notifications as read
|
- Mark all vendor notifications as read
|
||||||
- Update timestamps
|
- Update timestamps
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Mark all as read coming in Slice 5"}
|
return MessageResponse(message="Mark all as read coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{notification_id}")
|
@router.delete("/{notification_id}", response_model=MessageResponse)
|
||||||
def delete_notification(
|
def delete_notification(
|
||||||
notification_id: int,
|
notification_id: int,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -116,11 +118,11 @@ def delete_notification(
|
|||||||
- Delete single notification
|
- Delete single notification
|
||||||
- Verify notification belongs to vendor
|
- Verify notification belongs to vendor
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Notification deletion coming in Slice 5"}
|
return MessageResponse(message="Notification deletion coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/settings")
|
@router.get("/settings", response_model=NotificationSettingsResponse)
|
||||||
def get_notification_settings(
|
def get_notification_settings(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -133,18 +135,18 @@ def get_notification_settings(
|
|||||||
- Get in-app notification settings
|
- Get in-app notification settings
|
||||||
- Get notification types enabled/disabled
|
- Get notification types enabled/disabled
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return NotificationSettingsResponse(
|
||||||
"email_notifications": True,
|
email_notifications=True,
|
||||||
"in_app_notifications": True,
|
in_app_notifications=True,
|
||||||
"notification_types": {},
|
notification_types={},
|
||||||
"message": "Notification settings coming in Slice 5",
|
message="Notification settings coming in Slice 5",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/settings")
|
@router.put("/settings", response_model=MessageResponse)
|
||||||
def update_notification_settings(
|
def update_notification_settings(
|
||||||
settings: dict,
|
settings: NotificationSettingsUpdate,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -156,11 +158,11 @@ def update_notification_settings(
|
|||||||
- Update in-app notification settings
|
- Update in-app notification settings
|
||||||
- Enable/disable specific notification types
|
- Enable/disable specific notification types
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Notification settings update coming in Slice 5"}
|
return MessageResponse(message="Notification settings update coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/templates")
|
@router.get("/templates", response_model=NotificationTemplateListResponse)
|
||||||
def get_notification_templates(
|
def get_notification_templates(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -173,14 +175,16 @@ def get_notification_templates(
|
|||||||
- Include: order confirmation, shipping notification, etc.
|
- Include: order confirmation, shipping notification, etc.
|
||||||
- Return template details
|
- Return template details
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"templates": [], "message": "Notification templates coming in Slice 5"}
|
return NotificationTemplateListResponse(
|
||||||
|
templates=[], message="Notification templates coming in Slice 5"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/templates/{template_id}")
|
@router.put("/templates/{template_id}", response_model=MessageResponse)
|
||||||
def update_notification_template(
|
def update_notification_template(
|
||||||
template_id: int,
|
template_id: int,
|
||||||
template_data: dict,
|
template_data: NotificationTemplateUpdate,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -193,13 +197,13 @@ def update_notification_template(
|
|||||||
- Validate template variables
|
- Validate template variables
|
||||||
- Preview template
|
- Preview template
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Template update coming in Slice 5"}
|
return MessageResponse(message="Template update coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/test")
|
@router.post("/test", response_model=MessageResponse)
|
||||||
def send_test_notification(
|
def send_test_notification(
|
||||||
notification_data: dict,
|
notification_data: TestNotificationRequest,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -211,5 +215,5 @@ def send_test_notification(
|
|||||||
- Use specified template
|
- Use specified template
|
||||||
- Send to current user's email
|
- Send to current user's email
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Test notification coming in Slice 5"}
|
return MessageResponse(message="Test notification coming in Slice 5")
|
||||||
|
|||||||
47
app/api/v1/vendor/orders.py
vendored
47
app/api/v1/vendor/orders.py
vendored
@@ -1,6 +1,9 @@
|
|||||||
# app/api/v1/vendor/orders.py
|
# app/api/v1/vendor/orders.py
|
||||||
"""
|
"""
|
||||||
Vendor order management endpoints.
|
Vendor order management endpoints.
|
||||||
|
|
||||||
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -42,20 +45,9 @@ def get_vendor_orders(
|
|||||||
Vendor is determined from JWT token (vendor_id claim).
|
Vendor is determined from JWT token (vendor_id claim).
|
||||||
Requires Authorization header (API endpoint).
|
Requires Authorization header (API endpoint).
|
||||||
"""
|
"""
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
orders, total = order_service.get_vendor_orders(
|
orders, total = order_service.get_vendor_orders(
|
||||||
db=db,
|
db=db,
|
||||||
vendor_id=vendor_id,
|
vendor_id=current_user.token_vendor_id,
|
||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
status=status,
|
status=status,
|
||||||
@@ -81,18 +73,9 @@ def get_order_details(
|
|||||||
|
|
||||||
Requires Authorization header (API endpoint).
|
Requires Authorization header (API endpoint).
|
||||||
"""
|
"""
|
||||||
from fastapi import HTTPException
|
order = order_service.get_order(
|
||||||
|
db=db, vendor_id=current_user.token_vendor_id, order_id=order_id
|
||||||
# Get vendor ID from token
|
)
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
order = order_service.get_order(db=db, vendor_id=vendor_id, order_id=order_id)
|
|
||||||
|
|
||||||
return OrderDetailResponse.model_validate(order)
|
return OrderDetailResponse.model_validate(order)
|
||||||
|
|
||||||
@@ -117,19 +100,11 @@ def update_order_status(
|
|||||||
|
|
||||||
Requires Authorization header (API endpoint).
|
Requires Authorization header (API endpoint).
|
||||||
"""
|
"""
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
order = order_service.update_order_status(
|
order = order_service.update_order_status(
|
||||||
db=db, vendor_id=vendor_id, order_id=order_id, order_update=order_update
|
db=db,
|
||||||
|
vendor_id=current_user.token_vendor_id,
|
||||||
|
order_id=order_id,
|
||||||
|
order_update=order_update,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
116
app/api/v1/vendor/payments.py
vendored
116
app/api/v1/vendor/payments.py
vendored
@@ -1,9 +1,9 @@
|
|||||||
# Payment configuration and processing
|
|
||||||
# app/api/v1/vendor/payments.py
|
# app/api/v1/vendor/payments.py
|
||||||
"""
|
"""
|
||||||
Vendor payment configuration and processing endpoints.
|
Vendor payment configuration and processing endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,22 +13,27 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.payment import (
|
||||||
|
PaymentBalanceResponse,
|
||||||
|
PaymentConfigResponse,
|
||||||
|
PaymentConfigUpdate,
|
||||||
|
PaymentConfigUpdateResponse,
|
||||||
|
PaymentMethodsResponse,
|
||||||
|
RefundRequest,
|
||||||
|
RefundResponse,
|
||||||
|
StripeConnectRequest,
|
||||||
|
StripeConnectResponse,
|
||||||
|
StripeDisconnectResponse,
|
||||||
|
TransactionsResponse,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/payments")
|
router = APIRouter(prefix="/payments")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
@router.get("/config", response_model=PaymentConfigResponse)
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config")
|
|
||||||
def get_payment_configuration(
|
def get_payment_configuration(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -42,19 +47,19 @@ def get_payment_configuration(
|
|||||||
- Get currency settings
|
- Get currency settings
|
||||||
- Return masked/secure information only
|
- Return masked/secure information only
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return PaymentConfigResponse(
|
||||||
"payment_gateway": None,
|
payment_gateway=None,
|
||||||
"accepted_methods": [],
|
accepted_methods=[],
|
||||||
"currency": "EUR",
|
currency="EUR",
|
||||||
"stripe_connected": False,
|
stripe_connected=False,
|
||||||
"message": "Payment configuration coming in Slice 5",
|
message="Payment configuration coming in Slice 5",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/config")
|
@router.put("/config", response_model=PaymentConfigUpdateResponse)
|
||||||
def update_payment_configuration(
|
def update_payment_configuration(
|
||||||
payment_config: dict,
|
payment_config: PaymentConfigUpdate,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -67,13 +72,15 @@ def update_payment_configuration(
|
|||||||
- Update accepted payment methods
|
- Update accepted payment methods
|
||||||
- Validate configuration before saving
|
- Validate configuration before saving
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Payment configuration update coming in Slice 5"}
|
return PaymentConfigUpdateResponse(
|
||||||
|
message="Payment configuration update coming in Slice 5"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/stripe/connect")
|
@router.post("/stripe/connect", response_model=StripeConnectResponse)
|
||||||
def connect_stripe_account(
|
def connect_stripe_account(
|
||||||
stripe_data: dict,
|
stripe_data: StripeConnectRequest,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -86,11 +93,11 @@ def connect_stripe_account(
|
|||||||
- Verify Stripe account is active
|
- Verify Stripe account is active
|
||||||
- Enable payment processing
|
- Enable payment processing
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Stripe connection coming in Slice 5"}
|
return StripeConnectResponse(message="Stripe connection coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/stripe/disconnect")
|
@router.delete("/stripe/disconnect", response_model=StripeDisconnectResponse)
|
||||||
def disconnect_stripe_account(
|
def disconnect_stripe_account(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -103,11 +110,11 @@ def disconnect_stripe_account(
|
|||||||
- Disable payment processing
|
- Disable payment processing
|
||||||
- Warn about pending payments
|
- Warn about pending payments
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Stripe disconnection coming in Slice 5"}
|
return StripeDisconnectResponse(message="Stripe disconnection coming in Slice 5")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/methods")
|
@router.get("/methods", response_model=PaymentMethodsResponse)
|
||||||
def get_payment_methods(
|
def get_payment_methods(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -119,11 +126,14 @@ def get_payment_methods(
|
|||||||
- Return list of enabled payment methods
|
- Return list of enabled payment methods
|
||||||
- Include: credit card, PayPal, bank transfer, etc.
|
- Include: credit card, PayPal, bank transfer, etc.
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"methods": [], "message": "Payment methods coming in Slice 5"}
|
return PaymentMethodsResponse(
|
||||||
|
methods=[],
|
||||||
|
message="Payment methods coming in Slice 5",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/transactions")
|
@router.get("/transactions", response_model=TransactionsResponse)
|
||||||
def get_payment_transactions(
|
def get_payment_transactions(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -137,15 +147,15 @@ def get_payment_transactions(
|
|||||||
- Include payment details
|
- Include payment details
|
||||||
- Support pagination
|
- Support pagination
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return TransactionsResponse(
|
||||||
"transactions": [],
|
transactions=[],
|
||||||
"total": 0,
|
total=0,
|
||||||
"message": "Payment transactions coming in Slice 5",
|
message="Payment transactions coming in Slice 5",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/balance")
|
@router.get("/balance", response_model=PaymentBalanceResponse)
|
||||||
def get_payment_balance(
|
def get_payment_balance(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -159,20 +169,20 @@ def get_payment_balance(
|
|||||||
- Get next payout date
|
- Get next payout date
|
||||||
- Get payout history
|
- Get payout history
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {
|
return PaymentBalanceResponse(
|
||||||
"available_balance": 0.0,
|
available_balance=0.0,
|
||||||
"pending_balance": 0.0,
|
pending_balance=0.0,
|
||||||
"currency": "EUR",
|
currency="EUR",
|
||||||
"next_payout_date": None,
|
next_payout_date=None,
|
||||||
"message": "Payment balance coming in Slice 5",
|
message="Payment balance coming in Slice 5",
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refund/{payment_id}")
|
@router.post("/refund/{payment_id}", response_model=RefundResponse)
|
||||||
def refund_payment(
|
def refund_payment(
|
||||||
payment_id: int,
|
payment_id: int,
|
||||||
refund_data: dict,
|
refund_data: RefundRequest,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -185,5 +195,5 @@ def refund_payment(
|
|||||||
- Update order status
|
- Update order status
|
||||||
- Send refund notification to customer
|
- Send refund notification to customer
|
||||||
"""
|
"""
|
||||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
|
||||||
return {"message": "Payment refund coming in Slice 5"}
|
return RefundResponse(message="Payment refund coming in Slice 5")
|
||||||
|
|||||||
77
app/api/v1/vendor/products.py
vendored
77
app/api/v1/vendor/products.py
vendored
@@ -1,6 +1,9 @@
|
|||||||
# app/api/v1/vendor/products.py
|
# app/api/v1/vendor/products.py
|
||||||
"""
|
"""
|
||||||
Vendor product catalog management endpoints.
|
Vendor product catalog management endpoints.
|
||||||
|
|
||||||
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -10,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InvalidTokenException
|
|
||||||
from app.services.product_service import product_service
|
from app.services.product_service import product_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.product import (
|
from models.schema.product import (
|
||||||
@@ -45,15 +47,9 @@ def get_vendor_products(
|
|||||||
|
|
||||||
Vendor is determined from JWT token (vendor_id claim).
|
Vendor is determined from JWT token (vendor_id claim).
|
||||||
"""
|
"""
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
products, total = product_service.get_vendor_products(
|
products, total = product_service.get_vendor_products(
|
||||||
db=db,
|
db=db,
|
||||||
vendor_id=vendor_id,
|
vendor_id=current_user.token_vendor_id,
|
||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
@@ -75,14 +71,8 @@ def get_product_details(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get detailed product information including inventory."""
|
"""Get detailed product information including inventory."""
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product = product_service.get_product(
|
product = product_service.get_product(
|
||||||
db=db, vendor_id=vendor_id, product_id=product_id
|
db=db, vendor_id=current_user.token_vendor_id, product_id=product_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return ProductDetailResponse.model_validate(product)
|
return ProductDetailResponse.model_validate(product)
|
||||||
@@ -99,14 +89,8 @@ def add_product_to_catalog(
|
|||||||
|
|
||||||
This publishes a MarketplaceProduct to the vendor's public catalog.
|
This publishes a MarketplaceProduct to the vendor's public catalog.
|
||||||
"""
|
"""
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product = product_service.create_product(
|
product = product_service.create_product(
|
||||||
db=db, vendor_id=vendor_id, product_data=product_data
|
db=db, vendor_id=current_user.token_vendor_id, product_data=product_data
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -125,14 +109,11 @@ def update_product(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update product in vendor catalog."""
|
"""Update product in vendor catalog."""
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product = product_service.update_product(
|
product = product_service.update_product(
|
||||||
db=db, vendor_id=vendor_id, product_id=product_id, product_update=product_data
|
db=db,
|
||||||
|
vendor_id=current_user.token_vendor_id,
|
||||||
|
product_id=product_id,
|
||||||
|
product_update=product_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -150,13 +131,9 @@ def remove_product_from_catalog(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Remove product from vendor catalog."""
|
"""Remove product from vendor catalog."""
|
||||||
# Get vendor ID from token
|
product_service.delete_product(
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
db=db, vendor_id=current_user.token_vendor_id, product_id=product_id
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
)
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product_service.delete_product(db=db, vendor_id=vendor_id, product_id=product_id)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Product {product_id} removed from catalog by user {current_user.username} "
|
f"Product {product_id} removed from catalog by user {current_user.username} "
|
||||||
@@ -177,18 +154,12 @@ def publish_from_marketplace(
|
|||||||
|
|
||||||
Shortcut endpoint for publishing directly from marketplace import.
|
Shortcut endpoint for publishing directly from marketplace import.
|
||||||
"""
|
"""
|
||||||
# Get vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product_data = ProductCreate(
|
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(
|
product = product_service.create_product(
|
||||||
db=db, vendor_id=vendor_id, product_data=product_data
|
db=db, vendor_id=current_user.token_vendor_id, product_data=product_data
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -206,13 +177,9 @@ def toggle_product_active(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Toggle product active status."""
|
"""Toggle product active status."""
|
||||||
# Get vendor ID from token
|
product = product_service.get_product(
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
db, current_user.token_vendor_id, product_id
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
)
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product = product_service.get_product(db, vendor_id, product_id)
|
|
||||||
|
|
||||||
product.is_active = not product.is_active
|
product.is_active = not product.is_active
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -231,13 +198,9 @@ def toggle_product_featured(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Toggle product featured status."""
|
"""Toggle product featured status."""
|
||||||
# Get vendor ID from token
|
product = product_service.get_product(
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
db, current_user.token_vendor_id, product_id
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
)
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
product = product_service.get_product(db, vendor_id, product_id)
|
|
||||||
|
|
||||||
product.is_featured = not product.is_featured
|
product.is_featured = not product.is_featured
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
24
app/api/v1/vendor/profile.py
vendored
24
app/api/v1/vendor/profile.py
vendored
@@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Vendor profile management endpoints.
|
Vendor profile management endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InsufficientPermissionsException, InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.vendor import VendorResponse, VendorUpdate
|
from models.schema.vendor import VendorResponse, VendorUpdate
|
||||||
@@ -21,20 +21,13 @@ router = APIRouter(prefix="/profile")
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_from_token(current_user: User, db: Session):
|
|
||||||
"""Helper to get vendor from JWT token."""
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=VendorResponse)
|
@router.get("", response_model=VendorResponse)
|
||||||
def get_vendor_profile(
|
def get_vendor_profile(
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get current vendor profile information."""
|
"""Get current vendor profile information."""
|
||||||
vendor = _get_vendor_from_token(current_user, db)
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||||
return vendor
|
return vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -45,10 +38,7 @@ def update_vendor_profile(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update vendor profile information."""
|
"""Update vendor profile information."""
|
||||||
vendor = _get_vendor_from_token(current_user, db)
|
# Service handles permission checking and raises InsufficientPermissionsException if needed
|
||||||
|
return vendor_service.update_vendor(
|
||||||
# Verify user has permission to update vendor
|
db, current_user.token_vendor_id, vendor_update, current_user
|
||||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
)
|
||||||
raise InsufficientPermissionsException(required_permission="vendor:profile:update")
|
|
||||||
|
|
||||||
return vendor_service.update_vendor(db, vendor.id, vendor_update)
|
|
||||||
|
|||||||
41
app/api/v1/vendor/settings.py
vendored
41
app/api/v1/vendor/settings.py
vendored
@@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Vendor settings and configuration endpoints.
|
Vendor settings and configuration endpoints.
|
||||||
|
|
||||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||||
|
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +13,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api
|
from app.api.deps import get_current_vendor_api
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.exceptions import InsufficientPermissionsException, InvalidTokenException
|
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
|
||||||
@@ -26,10 +26,6 @@ def get_vendor_settings(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get vendor settings and configuration."""
|
"""Get vendor settings and configuration."""
|
||||||
# Get vendor ID from JWT token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
|
||||||
|
|
||||||
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -56,32 +52,7 @@ def update_marketplace_settings(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update marketplace integration settings."""
|
"""Update marketplace integration settings."""
|
||||||
# Get vendor ID from JWT token
|
# Service handles permission checking and raises InsufficientPermissionsException if needed
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
return vendor_service.update_marketplace_settings(
|
||||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
db, current_user.token_vendor_id, marketplace_config, current_user
|
||||||
|
)
|
||||||
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
|
||||||
|
|
||||||
# Verify permissions
|
|
||||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
|
||||||
raise InsufficientPermissionsException(
|
|
||||||
required_permission="vendor:settings:update"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update Letzshop URLs
|
|
||||||
if "letzshop_csv_url_fr" in marketplace_config:
|
|
||||||
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
|
||||||
if "letzshop_csv_url_en" in marketplace_config:
|
|
||||||
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
|
||||||
if "letzshop_csv_url_de" in marketplace_config:
|
|
||||||
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
db.refresh(vendor)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": "Marketplace settings updated successfully",
|
|
||||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
|
||||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
|
||||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
|
||||||
}
|
|
||||||
|
|||||||
2
app/api/v1/vendor/team.py
vendored
2
app/api/v1/vendor/team.py
vendored
@@ -164,7 +164,7 @@ def invite_team_member(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/accept-invitation", response_model=InvitationAcceptResponse)
|
@router.post("/accept-invitation", response_model=InvitationAcceptResponse) # public
|
||||||
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.
|
Accept a team invitation and activate account.
|
||||||
|
|||||||
@@ -94,6 +94,50 @@ class MarketplaceImportJobService:
|
|||||||
logger.error(f"Error getting import job {job_id}: {str(e)}")
|
logger.error(f"Error getting import job {job_id}: {str(e)}")
|
||||||
raise ValidationException("Failed to retrieve import job")
|
raise ValidationException("Failed to retrieve import job")
|
||||||
|
|
||||||
|
def get_import_job_for_vendor(
|
||||||
|
self, db: Session, job_id: int, vendor_id: int
|
||||||
|
) -> MarketplaceImportJob:
|
||||||
|
"""
|
||||||
|
Get a marketplace import job by ID with vendor access control.
|
||||||
|
|
||||||
|
Validates that the job belongs to the specified vendor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database session
|
||||||
|
job_id: Import job ID
|
||||||
|
vendor_id: Vendor ID from token (to verify ownership)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImportJobNotFoundException: If job not found
|
||||||
|
UnauthorizedVendorAccessException: If job doesn't belong to vendor
|
||||||
|
"""
|
||||||
|
from app.exceptions import UnauthorizedVendorAccessException
|
||||||
|
|
||||||
|
try:
|
||||||
|
job = (
|
||||||
|
db.query(MarketplaceImportJob)
|
||||||
|
.filter(MarketplaceImportJob.id == job_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
raise ImportJobNotFoundException(job_id)
|
||||||
|
|
||||||
|
# Verify job belongs to vendor (service layer validation)
|
||||||
|
if job.vendor_id != vendor_id:
|
||||||
|
raise UnauthorizedVendorAccessException(
|
||||||
|
vendor_code=str(vendor_id),
|
||||||
|
user_id=0, # Not user-specific, but vendor mismatch
|
||||||
|
)
|
||||||
|
|
||||||
|
return job
|
||||||
|
|
||||||
|
except (ImportJobNotFoundException, UnauthorizedVendorAccessException):
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting import job {job_id} for vendor {vendor_id}: {str(e)}")
|
||||||
|
raise ValidationException("Failed to retrieve import job")
|
||||||
|
|
||||||
def get_import_jobs(
|
def get_import_jobs(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
|
|||||||
@@ -252,6 +252,44 @@ class VendorService:
|
|||||||
|
|
||||||
return vendor
|
return vendor
|
||||||
|
|
||||||
|
def get_active_vendor_by_code(self, db: Session, vendor_code: str) -> Vendor:
|
||||||
|
"""
|
||||||
|
Get active vendor by vendor_code for public access (no auth required).
|
||||||
|
|
||||||
|
This method is specifically designed for public endpoints where:
|
||||||
|
- No authentication is required
|
||||||
|
- Only active vendors should be returned
|
||||||
|
- Inactive/disabled vendors are hidden
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database session
|
||||||
|
vendor_code: Vendor code (case-insensitive)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Vendor object with company and owner loaded
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
VendorNotFoundException: If vendor not found or inactive
|
||||||
|
"""
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
from models.database.company import Company
|
||||||
|
|
||||||
|
vendor = (
|
||||||
|
db.query(Vendor)
|
||||||
|
.options(joinedload(Vendor.company).joinedload(Company.owner))
|
||||||
|
.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}")
|
||||||
|
raise VendorNotFoundException(vendor_code, identifier_type="code")
|
||||||
|
|
||||||
|
return vendor
|
||||||
|
|
||||||
def get_vendor_by_identifier(self, db: Session, identifier: str) -> Vendor:
|
def get_vendor_by_identifier(self, db: Session, identifier: str) -> Vendor:
|
||||||
"""
|
"""
|
||||||
Get vendor by ID or vendor_code (admin use - no access control).
|
Get vendor by ID or vendor_code (admin use - no access control).
|
||||||
@@ -544,6 +582,107 @@ class VendorService:
|
|||||||
"""Check if user is vendor owner (via company ownership)."""
|
"""Check if user is vendor owner (via company ownership)."""
|
||||||
return vendor.company and vendor.company.owner_user_id == user.id
|
return vendor.company and vendor.company.owner_user_id == user.id
|
||||||
|
|
||||||
|
def can_update_vendor(self, vendor: Vendor, user: User) -> bool:
|
||||||
|
"""
|
||||||
|
Check if user has permission to update vendor settings.
|
||||||
|
|
||||||
|
Permission granted to:
|
||||||
|
- Admins (always)
|
||||||
|
- Vendor owners (company owner)
|
||||||
|
- Team members with appropriate role (owner role in VendorUser)
|
||||||
|
"""
|
||||||
|
# Admins can always update
|
||||||
|
if user.role == "admin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if user is vendor owner via company
|
||||||
|
if self._is_vendor_owner(vendor, user):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if user is owner via VendorUser relationship
|
||||||
|
if user.is_owner_of(vendor.id):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_vendor(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
vendor_id: int,
|
||||||
|
vendor_update,
|
||||||
|
current_user: User,
|
||||||
|
) -> "Vendor":
|
||||||
|
"""
|
||||||
|
Update vendor profile with permission checking.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
VendorNotFoundException: If vendor not found
|
||||||
|
InsufficientPermissionsException: If user lacks permission
|
||||||
|
"""
|
||||||
|
from app.exceptions import InsufficientPermissionsException
|
||||||
|
|
||||||
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
||||||
|
|
||||||
|
# Check permissions in service layer
|
||||||
|
if not self.can_update_vendor(vendor, current_user):
|
||||||
|
raise InsufficientPermissionsException(
|
||||||
|
required_permission="vendor:profile:update"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
update_data = vendor_update.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
if hasattr(vendor, field):
|
||||||
|
setattr(vendor, field, value)
|
||||||
|
|
||||||
|
db.add(vendor)
|
||||||
|
db.flush()
|
||||||
|
db.refresh(vendor)
|
||||||
|
return vendor
|
||||||
|
|
||||||
|
def update_marketplace_settings(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
vendor_id: int,
|
||||||
|
marketplace_config: dict,
|
||||||
|
current_user: User,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Update marketplace integration settings with permission checking.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
VendorNotFoundException: If vendor not found
|
||||||
|
InsufficientPermissionsException: If user lacks permission
|
||||||
|
"""
|
||||||
|
from app.exceptions import InsufficientPermissionsException
|
||||||
|
|
||||||
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
||||||
|
|
||||||
|
# Check permissions in service layer
|
||||||
|
if not self.can_update_vendor(vendor, current_user):
|
||||||
|
raise InsufficientPermissionsException(
|
||||||
|
required_permission="vendor:settings:update"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update Letzshop URLs
|
||||||
|
if "letzshop_csv_url_fr" in marketplace_config:
|
||||||
|
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
||||||
|
if "letzshop_csv_url_en" in marketplace_config:
|
||||||
|
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
||||||
|
if "letzshop_csv_url_de" in marketplace_config:
|
||||||
|
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
||||||
|
|
||||||
|
db.add(vendor)
|
||||||
|
db.flush()
|
||||||
|
db.refresh(vendor)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Marketplace settings updated successfully",
|
||||||
|
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||||
|
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||||
|
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Create service instance following the same pattern as other services
|
# Create service instance following the same pattern as other services
|
||||||
vendor_service = VendorService()
|
vendor_service = VendorService()
|
||||||
|
|||||||
@@ -453,14 +453,37 @@ current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
|||||||
|
|
||||||
**Purpose:** Authenticate vendor users for API endpoints
|
**Purpose:** Authenticate vendor users for API endpoints
|
||||||
**Accepts:** Authorization header ONLY
|
**Accepts:** Authorization header ONLY
|
||||||
**Returns:** `User` object with `role="vendor"`
|
**Returns:** `User` object with `role="vendor"` and **guaranteed** attributes:
|
||||||
|
- `current_user.token_vendor_id` - Vendor ID from JWT token
|
||||||
|
- `current_user.token_vendor_code` - Vendor code from JWT token
|
||||||
|
- `current_user.token_vendor_role` - User's role in vendor (owner, manager, etc.)
|
||||||
|
|
||||||
**Raises:**
|
**Raises:**
|
||||||
- `InvalidTokenException` - No token or invalid token
|
- `InvalidTokenException` - No token, invalid token, or **missing vendor context in token**
|
||||||
- `InsufficientPermissionsException` - User is not vendor or is admin
|
- `InsufficientPermissionsException` - User is not vendor, is admin, or lost access to vendor
|
||||||
|
|
||||||
|
**Guarantees:**
|
||||||
|
This dependency **guarantees** that `token_vendor_id` is present. Endpoints should NOT check for its existence:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ WRONG - Redundant check violates API-003
|
||||||
|
if not hasattr(current_user, "token_vendor_id"):
|
||||||
|
raise InvalidTokenException("...")
|
||||||
|
|
||||||
|
# ✅ CORRECT - Dependency guarantees this attribute exists
|
||||||
|
vendor_id = current_user.token_vendor_id
|
||||||
|
```
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```python
|
```python
|
||||||
current_user: User = Depends(get_current_vendor_api)
|
@router.get("/orders")
|
||||||
|
def get_orders(
|
||||||
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
# Safe to use directly - dependency guarantees token_vendor_id
|
||||||
|
orders = order_service.get_vendor_orders(db, current_user.token_vendor_id)
|
||||||
|
return orders
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `get_current_customer_from_cookie_or_header()`
|
#### `get_current_customer_from_cookie_or_header()`
|
||||||
|
|||||||
@@ -195,27 +195,20 @@ def get_current_vendor_api(
|
|||||||
def get_vendor_products(
|
def get_vendor_products(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
|
current_user: User = Depends(get_current_vendor_api), # ✅ Guarantees token_vendor_id
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get all products in vendor catalog.
|
Get all products in vendor catalog.
|
||||||
|
|
||||||
Vendor is determined from JWT token (vendor_id claim).
|
Vendor is determined from JWT token (vendor_id claim).
|
||||||
|
The get_current_vendor_api dependency GUARANTEES token_vendor_id is present.
|
||||||
"""
|
"""
|
||||||
# Extract vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
# Use vendor_id from token for business logic
|
# Use vendor_id from token for business logic
|
||||||
|
# NO validation needed - dependency guarantees token_vendor_id exists
|
||||||
products, total = product_service.get_vendor_products(
|
products, total = product_service.get_vendor_products(
|
||||||
db=db,
|
db=db,
|
||||||
vendor_id=vendor_id,
|
vendor_id=current_user.token_vendor_id, # Safe to use directly
|
||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
@@ -223,6 +216,9 @@ def get_vendor_products(
|
|||||||
return ProductListResponse(products=products, total=total)
|
return ProductListResponse(products=products, total=total)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **IMPORTANT**: The `get_current_vendor_api()` dependency now **guarantees** that `token_vendor_id` is present.
|
||||||
|
> Endpoints should NOT check for its existence - this would be redundant validation that belongs in the dependency layer.
|
||||||
|
|
||||||
## Migration Guide
|
## Migration Guide
|
||||||
|
|
||||||
### Step 1: Identify Endpoints Using require_vendor_context()
|
### Step 1: Identify Endpoints Using require_vendor_context()
|
||||||
@@ -264,21 +260,14 @@ product = product_service.get_product(db, vendor.id, product_id)
|
|||||||
|
|
||||||
**After:**
|
**After:**
|
||||||
```python
|
```python
|
||||||
from fastapi import HTTPException
|
# Use vendor_id from token directly - dependency guarantees it exists
|
||||||
|
product = product_service.get_product(db, current_user.token_vendor_id, product_id)
|
||||||
# Extract vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id
|
|
||||||
|
|
||||||
# Use vendor_id from token
|
|
||||||
product = product_service.get_product(db, vendor_id, product_id)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **NOTE**: Do NOT add validation like `if not hasattr(current_user, "token_vendor_id")`.
|
||||||
|
> The `get_current_vendor_api` dependency guarantees this attribute is present.
|
||||||
|
> Adding such checks violates the architecture rule API-003 (endpoints should not raise exceptions).
|
||||||
|
|
||||||
### Step 4: Update Logging References
|
### Step 4: Update Logging References
|
||||||
|
|
||||||
**Before:**
|
**Before:**
|
||||||
@@ -325,24 +314,14 @@ def update_product(
|
|||||||
def update_product(
|
def update_product(
|
||||||
product_id: int,
|
product_id: int,
|
||||||
product_data: ProductUpdate,
|
product_data: ProductUpdate,
|
||||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only dependency
|
current_user: User = Depends(get_current_vendor_api), # ✅ Guarantees token_vendor_id
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update product in vendor catalog."""
|
"""Update product in vendor catalog."""
|
||||||
from fastapi import HTTPException
|
# NO validation needed - dependency guarantees token_vendor_id exists
|
||||||
|
|
||||||
# Extract vendor ID from token
|
|
||||||
if not hasattr(current_user, "token_vendor_id"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Token missing vendor information. Please login again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor_id = current_user.token_vendor_id # ✅ From token
|
|
||||||
|
|
||||||
product = product_service.update_product(
|
product = product_service.update_product(
|
||||||
db=db,
|
db=db,
|
||||||
vendor_id=vendor_id, # ✅ From token
|
vendor_id=current_user.token_vendor_id, # ✅ From token - safe to use directly
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
product_update=product_data
|
product_update=product_data
|
||||||
)
|
)
|
||||||
@@ -355,6 +334,9 @@ def update_product(
|
|||||||
return ProductResponse.model_validate(product)
|
return ProductResponse.model_validate(product)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Architecture Rule API-003**: Endpoints should NOT raise exceptions. The `get_current_vendor_api` dependency
|
||||||
|
> handles all validation and raises `InvalidTokenException` if `token_vendor_id` is missing.
|
||||||
|
|
||||||
## Migration Status
|
## Migration Status
|
||||||
|
|
||||||
**COMPLETED** - All vendor API endpoints have been migrated to use the token-based vendor context pattern.
|
**COMPLETED** - All vendor API endpoints have been migrated to use the token-based vendor context pattern.
|
||||||
@@ -498,9 +480,81 @@ def test_vendor_login_and_api_access():
|
|||||||
# All products should belong to token vendor
|
# All products should belong to token vendor
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Rules
|
## Architecture Rules and Design Pattern Enforcement
|
||||||
|
|
||||||
See `docs/architecture/rules/API-VND-001.md` for the formal architecture rule enforcing this pattern.
|
### The Layered Exception Pattern
|
||||||
|
|
||||||
|
The architecture enforces a strict layered pattern for where exceptions should be raised:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ENDPOINTS (Thin Layer) - app/api/v1/**/*.py │
|
||||||
|
│ │
|
||||||
|
│ ❌ MUST NOT raise exceptions │
|
||||||
|
│ ❌ MUST NOT check hasattr(current_user, 'token_vendor_id') │
|
||||||
|
│ ✅ MUST trust dependencies to handle validation │
|
||||||
|
│ ✅ MUST directly use current_user.token_vendor_id │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DEPENDENCIES (Validation Layer) - app/api/deps.py │
|
||||||
|
│ │
|
||||||
|
│ ✅ MUST raise InvalidTokenException if token_vendor_id missing │
|
||||||
|
│ ✅ MUST validate user still has vendor access │
|
||||||
|
│ ✅ GUARANTEES token_vendor_id, token_vendor_code, token_vendor_role │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SERVICES (Business Logic) - app/services/**/*.py │
|
||||||
|
│ │
|
||||||
|
│ ✅ MUST raise domain exceptions for business rule violations │
|
||||||
|
│ ✅ Examples: VendorNotFoundException, ProductNotFoundException │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ GLOBAL EXCEPTION HANDLER - app/exceptions/handler.py │
|
||||||
|
│ │
|
||||||
|
│ ✅ Catches all WizamartException subclasses │
|
||||||
|
│ ✅ Converts to appropriate HTTP responses │
|
||||||
|
│ ✅ Provides consistent error formatting │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enforced by Architecture Validation
|
||||||
|
|
||||||
|
The validation script (`scripts/validate_architecture.py`) enforces these rules:
|
||||||
|
|
||||||
|
**Rule API-003: Endpoints must NOT raise exceptions directly**
|
||||||
|
- Detects `raise HTTPException`, `raise InvalidTokenException`, etc. in endpoint files
|
||||||
|
- Detects redundant validation like `if not hasattr(current_user, 'token_vendor_id')`
|
||||||
|
- Blocks commits via pre-commit hook if violations found
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
|
||||||
|
Architecture validation runs on every commit:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .pre-commit-config.yaml
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: validate-architecture
|
||||||
|
name: Validate Architecture Patterns
|
||||||
|
entry: python scripts/validate_architecture.py
|
||||||
|
language: python
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
```
|
||||||
|
|
||||||
|
To run manually:
|
||||||
|
```bash
|
||||||
|
python scripts/validate_architecture.py # Full validation
|
||||||
|
python scripts/validate_architecture.py -d app/api/v1/vendor/ # Specific directory
|
||||||
|
```
|
||||||
|
|
||||||
|
See `.architecture-rules.yaml` for the complete rule definitions.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
@@ -519,4 +573,4 @@ The vendor-in-token architecture:
|
|||||||
- ✅ Simplifies endpoint implementation
|
- ✅ Simplifies endpoint implementation
|
||||||
- ✅ Follows architecture best practices
|
- ✅ Follows architecture best practices
|
||||||
|
|
||||||
**Migration Status:** In progress - 9 endpoint files remaining to migrate
|
**Migration Status:** ✅ COMPLETED - All vendor API endpoints migrated and architecture rules enforced
|
||||||
|
|||||||
@@ -1 +1,190 @@
|
|||||||
# Media/file management models
|
# models/schema/media.py
|
||||||
|
"""
|
||||||
|
Media/file management Pydantic schemas for API validation and responses.
|
||||||
|
|
||||||
|
This module provides schemas for:
|
||||||
|
- Media library listing
|
||||||
|
- File upload responses
|
||||||
|
- Media metadata operations
|
||||||
|
- Media usage tracking
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SHARED RESPONSE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MessageResponse(BaseModel):
|
||||||
|
"""Generic message response for simple operations."""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MEDIA ITEM SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MediaItemResponse(BaseModel):
|
||||||
|
"""Single media item response."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
filename: str
|
||||||
|
original_filename: str | None = None
|
||||||
|
file_url: str
|
||||||
|
thumbnail_url: str | None = None
|
||||||
|
media_type: str # image, video, document
|
||||||
|
mime_type: str | None = None
|
||||||
|
file_size: int | None = None # bytes
|
||||||
|
width: int | None = None # for images/videos
|
||||||
|
height: int | None = None # for images/videos
|
||||||
|
alt_text: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
folder: str | None = None
|
||||||
|
metadata: dict[str, Any] | None = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class MediaListResponse(BaseModel):
|
||||||
|
"""Paginated list of media items."""
|
||||||
|
|
||||||
|
media: list[MediaItemResponse] = []
|
||||||
|
total: int = 0
|
||||||
|
skip: int = 0
|
||||||
|
limit: int = 100
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# UPLOAD RESPONSE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MediaUploadResponse(BaseModel):
|
||||||
|
"""Response for single file upload."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
file_url: str | None = None
|
||||||
|
thumbnail_url: str | None = None
|
||||||
|
filename: str | None = None
|
||||||
|
file_size: int | None = None
|
||||||
|
media_type: str | None = None
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UploadedFileInfo(BaseModel):
|
||||||
|
"""Information about a successfully uploaded file."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
filename: str
|
||||||
|
file_url: str
|
||||||
|
thumbnail_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FailedFileInfo(BaseModel):
|
||||||
|
"""Information about a failed file upload."""
|
||||||
|
|
||||||
|
filename: str
|
||||||
|
error: str
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleUploadResponse(BaseModel):
|
||||||
|
"""Response for multiple file upload."""
|
||||||
|
|
||||||
|
uploaded_files: list[UploadedFileInfo] = []
|
||||||
|
failed_files: list[FailedFileInfo] = []
|
||||||
|
total_uploaded: int = 0
|
||||||
|
total_failed: int = 0
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MEDIA DETAIL SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MediaDetailResponse(BaseModel):
|
||||||
|
"""Detailed media item response with usage info."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
filename: str | None = None
|
||||||
|
original_filename: str | None = None
|
||||||
|
file_url: str | None = None
|
||||||
|
thumbnail_url: str | None = None
|
||||||
|
media_type: str | None = None
|
||||||
|
mime_type: str | None = None
|
||||||
|
file_size: int | None = None
|
||||||
|
width: int | None = None
|
||||||
|
height: int | None = None
|
||||||
|
alt_text: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
folder: str | None = None
|
||||||
|
metadata: dict[str, Any] | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MEDIA UPDATE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MediaMetadataUpdate(BaseModel):
|
||||||
|
"""Request model for updating media metadata."""
|
||||||
|
|
||||||
|
filename: str | None = Field(None, max_length=255)
|
||||||
|
alt_text: str | None = Field(None, max_length=500)
|
||||||
|
description: str | None = None
|
||||||
|
folder: str | None = Field(None, max_length=100)
|
||||||
|
metadata: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MEDIA USAGE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ProductUsageInfo(BaseModel):
|
||||||
|
"""Information about product using this media."""
|
||||||
|
|
||||||
|
product_id: int
|
||||||
|
product_name: str
|
||||||
|
usage_type: str # main_image, gallery, variant, etc.
|
||||||
|
|
||||||
|
|
||||||
|
class MediaUsageResponse(BaseModel):
|
||||||
|
"""Response showing where media is being used."""
|
||||||
|
|
||||||
|
media_id: int | None = None
|
||||||
|
products: list[ProductUsageInfo] = []
|
||||||
|
other_usage: list[dict[str, Any]] = []
|
||||||
|
total_usage_count: int = 0
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MEDIA OPTIMIZATION SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizationResultResponse(BaseModel):
|
||||||
|
"""Response for media optimization operation."""
|
||||||
|
|
||||||
|
media_id: int | None = None
|
||||||
|
original_size: int | None = None
|
||||||
|
optimized_size: int | None = None
|
||||||
|
savings_percent: float | None = None
|
||||||
|
optimized_url: str | None = None
|
||||||
|
message: str | None = None
|
||||||
|
|||||||
@@ -1 +1,151 @@
|
|||||||
# Notification models
|
# models/schema/notification.py
|
||||||
|
"""
|
||||||
|
Notification Pydantic schemas for API validation and responses.
|
||||||
|
|
||||||
|
This module provides schemas for:
|
||||||
|
- Vendor notifications (list, read, delete)
|
||||||
|
- Notification settings management
|
||||||
|
- Notification email templates
|
||||||
|
- Unread counts and statistics
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SHARED RESPONSE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class MessageResponse(BaseModel):
|
||||||
|
"""Generic message response for simple operations."""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class UnreadCountResponse(BaseModel):
|
||||||
|
"""Response for unread notification count."""
|
||||||
|
|
||||||
|
unread_count: int
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# NOTIFICATION SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationResponse(BaseModel):
|
||||||
|
"""Single notification response."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
type: str
|
||||||
|
title: str
|
||||||
|
message: str
|
||||||
|
is_read: bool
|
||||||
|
read_at: datetime | None = None
|
||||||
|
priority: str = "normal"
|
||||||
|
action_url: str | None = None
|
||||||
|
metadata: dict[str, Any] | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationListResponse(BaseModel):
|
||||||
|
"""Paginated list of notifications."""
|
||||||
|
|
||||||
|
notifications: list[NotificationResponse] = []
|
||||||
|
total: int = 0
|
||||||
|
unread_count: int = 0
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# NOTIFICATION SETTINGS SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSettingsResponse(BaseModel):
|
||||||
|
"""Notification preferences response."""
|
||||||
|
|
||||||
|
email_notifications: bool = True
|
||||||
|
in_app_notifications: bool = True
|
||||||
|
notification_types: dict[str, bool] = Field(default_factory=dict)
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSettingsUpdate(BaseModel):
|
||||||
|
"""Request model for updating notification settings."""
|
||||||
|
|
||||||
|
email_notifications: bool | None = None
|
||||||
|
in_app_notifications: bool | None = None
|
||||||
|
notification_types: dict[str, bool] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# NOTIFICATION TEMPLATE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTemplateResponse(BaseModel):
|
||||||
|
"""Single notification template response."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
subject: str
|
||||||
|
body_html: str | None = None
|
||||||
|
body_text: str | None = None
|
||||||
|
variables: list[str] = Field(default_factory=list)
|
||||||
|
is_active: bool = True
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTemplateListResponse(BaseModel):
|
||||||
|
"""List of notification templates."""
|
||||||
|
|
||||||
|
templates: list[NotificationTemplateResponse] = []
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTemplateUpdate(BaseModel):
|
||||||
|
"""Request model for updating notification template."""
|
||||||
|
|
||||||
|
subject: str | None = Field(None, max_length=200)
|
||||||
|
body_html: str | None = None
|
||||||
|
body_text: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEST NOTIFICATION SCHEMA
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestNotificationRequest(BaseModel):
|
||||||
|
"""Request model for sending test notification."""
|
||||||
|
|
||||||
|
template_id: int | None = Field(None, description="Template to use")
|
||||||
|
email: str | None = Field(None, description="Override recipient email")
|
||||||
|
notification_type: str = Field(default="test", description="Type of notification to send")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ADMIN ALERT STATISTICS SCHEMA
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class AlertStatisticsResponse(BaseModel):
|
||||||
|
"""Response for alert statistics."""
|
||||||
|
|
||||||
|
total_alerts: int = 0
|
||||||
|
active_alerts: int = 0
|
||||||
|
critical_alerts: int = 0
|
||||||
|
resolved_today: int = 0
|
||||||
|
|||||||
@@ -1 +1,166 @@
|
|||||||
# Payment models
|
# models/schema/payment.py
|
||||||
|
"""
|
||||||
|
Payment Pydantic schemas for API validation and responses.
|
||||||
|
|
||||||
|
This module provides schemas for:
|
||||||
|
- Payment configuration
|
||||||
|
- Stripe integration
|
||||||
|
- Payment methods
|
||||||
|
- Transactions and balance
|
||||||
|
- Refunds
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PAYMENT CONFIGURATION SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentConfigResponse(BaseModel):
|
||||||
|
"""Response for payment configuration."""
|
||||||
|
|
||||||
|
payment_gateway: str | None = None
|
||||||
|
accepted_methods: list[str] = []
|
||||||
|
currency: str = "EUR"
|
||||||
|
stripe_connected: bool = False
|
||||||
|
stripe_account_id: str | None = None
|
||||||
|
paypal_connected: bool = False
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentConfigUpdate(BaseModel):
|
||||||
|
"""Request model for updating payment configuration."""
|
||||||
|
|
||||||
|
payment_gateway: str | None = Field(None, max_length=50)
|
||||||
|
accepted_methods: list[str] | None = None
|
||||||
|
currency: str | None = Field(None, max_length=3)
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentConfigUpdateResponse(BaseModel):
|
||||||
|
"""Response for payment configuration update."""
|
||||||
|
|
||||||
|
success: bool = False
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STRIPE INTEGRATION SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class StripeConnectRequest(BaseModel):
|
||||||
|
"""Request model for connecting Stripe account."""
|
||||||
|
|
||||||
|
authorization_code: str | None = None
|
||||||
|
state: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StripeConnectResponse(BaseModel):
|
||||||
|
"""Response for Stripe connection."""
|
||||||
|
|
||||||
|
connected: bool = False
|
||||||
|
stripe_account_id: str | None = None
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StripeDisconnectResponse(BaseModel):
|
||||||
|
"""Response for Stripe disconnection."""
|
||||||
|
|
||||||
|
disconnected: bool = False
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PAYMENT METHODS SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodInfo(BaseModel):
|
||||||
|
"""Information about a payment method."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
type: str # credit_card, paypal, bank_transfer, etc.
|
||||||
|
enabled: bool = True
|
||||||
|
icon: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodsResponse(BaseModel):
|
||||||
|
"""Response for payment methods listing."""
|
||||||
|
|
||||||
|
methods: list[PaymentMethodInfo] = []
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TRANSACTION SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionInfo(BaseModel):
|
||||||
|
"""Information about a payment transaction."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
order_id: int | None = None
|
||||||
|
amount: float
|
||||||
|
currency: str = "EUR"
|
||||||
|
status: str # pending, completed, failed, refunded
|
||||||
|
payment_method: str | None = None
|
||||||
|
customer_email: str | None = None
|
||||||
|
created_at: datetime
|
||||||
|
completed_at: datetime | None = None
|
||||||
|
metadata: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionsResponse(BaseModel):
|
||||||
|
"""Response for payment transactions listing."""
|
||||||
|
|
||||||
|
transactions: list[TransactionInfo] = []
|
||||||
|
total: int = 0
|
||||||
|
skip: int = 0
|
||||||
|
limit: int = 50
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# BALANCE SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentBalanceResponse(BaseModel):
|
||||||
|
"""Response for payment balance information."""
|
||||||
|
|
||||||
|
available_balance: float = 0.0
|
||||||
|
pending_balance: float = 0.0
|
||||||
|
currency: str = "EUR"
|
||||||
|
next_payout_date: datetime | None = None
|
||||||
|
last_payout_date: datetime | None = None
|
||||||
|
last_payout_amount: float | None = None
|
||||||
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# REFUND SCHEMAS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class RefundRequest(BaseModel):
|
||||||
|
"""Request model for processing a refund."""
|
||||||
|
|
||||||
|
amount: float | None = Field(None, gt=0, description="Partial refund amount, or None for full refund")
|
||||||
|
reason: str | None = Field(None, max_length=500)
|
||||||
|
|
||||||
|
|
||||||
|
class RefundResponse(BaseModel):
|
||||||
|
"""Response for refund operation."""
|
||||||
|
|
||||||
|
refund_id: int | None = None
|
||||||
|
payment_id: int | None = None
|
||||||
|
amount: float | None = None
|
||||||
|
status: str | None = None # pending, completed, failed
|
||||||
|
message: str | None = None
|
||||||
|
|||||||
@@ -517,39 +517,72 @@ class ArchitectureValidator:
|
|||||||
def _check_endpoint_exception_handling(
|
def _check_endpoint_exception_handling(
|
||||||
self, file_path: Path, content: str, lines: list[str]
|
self, file_path: Path, content: str, lines: list[str]
|
||||||
):
|
):
|
||||||
"""API-003: Check that endpoints do NOT raise HTTPException directly.
|
"""API-003: Check that endpoints do NOT raise exceptions directly.
|
||||||
|
|
||||||
The architecture uses a global exception handler that catches domain
|
The architecture uses:
|
||||||
exceptions (WizamartException subclasses) and converts them to HTTP
|
- Dependencies (deps.py) for authentication/authorization validation
|
||||||
responses. Endpoints should let exceptions bubble up, not catch and
|
- Services for business logic validation
|
||||||
convert them manually.
|
- Global exception handler that catches WizamartException subclasses
|
||||||
|
|
||||||
|
Endpoints should be a thin orchestration layer that trusts dependencies
|
||||||
|
and services to handle all validation. They should NOT raise exceptions.
|
||||||
"""
|
"""
|
||||||
rule = self._get_rule("API-003")
|
rule = self._get_rule("API-003")
|
||||||
if not rule:
|
if not rule:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Skip exception handler file - it's allowed to use HTTPException
|
# Skip exception handler file and deps.py - they're allowed to raise exceptions
|
||||||
if "exceptions/handler.py" in str(file_path):
|
file_path_str = str(file_path)
|
||||||
|
if "exceptions/handler.py" in file_path_str or file_path_str.endswith("deps.py"):
|
||||||
return
|
return
|
||||||
|
|
||||||
for i, line in enumerate(lines, 1):
|
# Patterns that indicate endpoints are raising exceptions (BAD)
|
||||||
# Check for raise HTTPException
|
exception_patterns = [
|
||||||
if "raise HTTPException" in line:
|
("raise HTTPException", "Endpoint raises HTTPException directly"),
|
||||||
# Skip if it's a comment
|
("raise InvalidTokenException", "Endpoint raises InvalidTokenException - move to dependency"),
|
||||||
stripped = line.strip()
|
("raise InsufficientPermissionsException", "Endpoint raises permission exception - move to dependency"),
|
||||||
if stripped.startswith("#"):
|
("raise UnauthorizedVendorAccessException", "Endpoint raises auth exception - move to dependency or service"),
|
||||||
continue
|
]
|
||||||
|
|
||||||
self._add_violation(
|
# Pattern that indicates redundant validation (BAD)
|
||||||
rule_id="API-003",
|
redundant_patterns = [
|
||||||
rule_name=rule["name"],
|
(r"if not hasattr\(current_user.*token_vendor", "Redundant token_vendor check - get_current_vendor_api guarantees this"),
|
||||||
severity=Severity.ERROR,
|
(r"if not hasattr\(current_user.*token_vendor_id", "Redundant token_vendor_id check - dependency guarantees this"),
|
||||||
file_path=file_path,
|
]
|
||||||
line_number=i,
|
|
||||||
message="Endpoint raises HTTPException directly",
|
for i, line in enumerate(lines, 1):
|
||||||
context=line.strip()[:80],
|
# Skip comments
|
||||||
suggestion="Use domain exceptions (e.g., VendorNotFoundException) and let global handler convert",
|
stripped = line.strip()
|
||||||
)
|
if stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for direct exception raising
|
||||||
|
for pattern, message in exception_patterns:
|
||||||
|
if pattern in line:
|
||||||
|
self._add_violation(
|
||||||
|
rule_id="API-003",
|
||||||
|
rule_name=rule["name"],
|
||||||
|
severity=Severity.ERROR,
|
||||||
|
file_path=file_path,
|
||||||
|
line_number=i,
|
||||||
|
message=message,
|
||||||
|
context=stripped[:80],
|
||||||
|
suggestion="Let dependencies or services handle validation and raise exceptions",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for redundant validation patterns
|
||||||
|
for pattern, message in redundant_patterns:
|
||||||
|
if re.search(pattern, line):
|
||||||
|
self._add_violation(
|
||||||
|
rule_id="API-003",
|
||||||
|
rule_name=rule["name"],
|
||||||
|
severity=Severity.ERROR,
|
||||||
|
file_path=file_path,
|
||||||
|
line_number=i,
|
||||||
|
message=message,
|
||||||
|
context=stripped[:80],
|
||||||
|
suggestion="Remove redundant check - auth dependency guarantees this attribute is present",
|
||||||
|
)
|
||||||
|
|
||||||
def _check_endpoint_authentication(
|
def _check_endpoint_authentication(
|
||||||
self, file_path: Path, content: str, lines: list[str]
|
self, file_path: Path, content: str, lines: list[str]
|
||||||
@@ -570,7 +603,21 @@ class ArchitectureValidator:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# This is a warning-level check
|
# This is a warning-level check
|
||||||
# Look for endpoints without Depends(get_current_*)
|
# Look for endpoints without proper authentication
|
||||||
|
# Valid auth patterns:
|
||||||
|
# - Depends(get_current_*) - direct user authentication
|
||||||
|
# - Depends(require_vendor_*) - vendor permission dependencies
|
||||||
|
# - Depends(require_any_vendor_*) - any permission check
|
||||||
|
# - Depends(require_all_vendor*) - all permissions check
|
||||||
|
# - Depends(get_user_permissions) - permission fetching
|
||||||
|
auth_patterns = [
|
||||||
|
"Depends(get_current_",
|
||||||
|
"Depends(require_vendor_",
|
||||||
|
"Depends(require_any_vendor_",
|
||||||
|
"Depends(require_all_vendor",
|
||||||
|
"Depends(get_user_permissions",
|
||||||
|
]
|
||||||
|
|
||||||
for i, line in enumerate(lines, 1):
|
for i, line in enumerate(lines, 1):
|
||||||
if "@router." in line and (
|
if "@router." in line and (
|
||||||
"post" in line or "put" in line or "delete" in line
|
"post" in line or "put" in line or "delete" in line
|
||||||
@@ -582,7 +629,8 @@ class ArchitectureValidator:
|
|||||||
context_lines = lines[i - 1 : i + 15] # Include line before decorator
|
context_lines = lines[i - 1 : i + 15] # Include line before decorator
|
||||||
|
|
||||||
for ctx_line in context_lines:
|
for ctx_line in context_lines:
|
||||||
if "Depends(get_current_" in ctx_line:
|
# Check for any valid auth pattern
|
||||||
|
if any(pattern in ctx_line for pattern in auth_patterns):
|
||||||
has_auth = True
|
has_auth = True
|
||||||
break
|
break
|
||||||
# Check for public endpoint markers
|
# Check for public endpoint markers
|
||||||
@@ -593,6 +641,17 @@ class ArchitectureValidator:
|
|||||||
if not has_auth and not is_public and "include_in_schema=False" not in " ".join(
|
if not has_auth and not is_public and "include_in_schema=False" not in " ".join(
|
||||||
lines[i : i + 15]
|
lines[i : i + 15]
|
||||||
):
|
):
|
||||||
|
# Determine appropriate suggestion based on file path
|
||||||
|
file_path_str = str(file_path)
|
||||||
|
if "/vendor/" in file_path_str:
|
||||||
|
suggestion = "Add Depends(get_current_vendor_api) or permission dependency, or mark as '# public'"
|
||||||
|
elif "/admin/" in file_path_str:
|
||||||
|
suggestion = "Add Depends(get_current_admin_api), or mark as '# public'"
|
||||||
|
elif "/shop/" in file_path_str:
|
||||||
|
suggestion = "Add Depends(get_current_customer_api), or mark as '# public'"
|
||||||
|
else:
|
||||||
|
suggestion = "Add authentication dependency or mark as '# public' if intentionally unauthenticated"
|
||||||
|
|
||||||
self._add_violation(
|
self._add_violation(
|
||||||
rule_id="API-004",
|
rule_id="API-004",
|
||||||
rule_name=rule["name"],
|
rule_name=rule["name"],
|
||||||
@@ -601,7 +660,7 @@ class ArchitectureValidator:
|
|||||||
line_number=i,
|
line_number=i,
|
||||||
message="Endpoint may be missing authentication",
|
message="Endpoint may be missing authentication",
|
||||||
context=line.strip(),
|
context=line.strip(),
|
||||||
suggestion="Add Depends(get_current_user) or mark as '# public' if intentionally unauthenticated",
|
suggestion=suggestion,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_service_layer(self, target_path: Path):
|
def _validate_service_layer(self, target_path: Path):
|
||||||
|
|||||||
Reference in New Issue
Block a user