refactor: migrate messaging and media routes to modules

Messaging module (communication):
- vendor_messages.py: Conversation and message management
- vendor_notifications.py: Vendor notifications
- vendor_email_settings.py: SMTP and provider configuration
- vendor_email_templates.py: Email template customization

CMS module (content management):
- vendor_media.py: Media library management
- vendor_content_pages.py: Content page overrides

All routes auto-discovered via is_self_contained=True.
Deleted 5 legacy files from app/api/v1/vendor/.
app/api/v1/vendor/ now empty except for __init__.py (auto-discovery only).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 15:21:25 +01:00
parent da3f28849e
commit 7639ca602b
12 changed files with 385 additions and 410 deletions

View File

@@ -21,44 +21,23 @@ Self-contained modules (auto-discovered from app/modules/{module}/routes/api/ven
- orders: Order management, fulfillment, exceptions, invoices
- marketplace: Letzshop integration, product sync, onboarding
- catalog: Vendor product catalog management
- cms: Content pages management
- cms: Content pages management, media library
- customers: Customer management
- payments: Payment configuration, Stripe connect, transactions
- tenancy: Vendor info, auth, profile, team management
- messaging: Messages, notifications, email settings, email templates
- core: Dashboard, settings
"""
from fastapi import APIRouter
# Import all sub-routers (legacy routes that haven't been migrated to modules)
from . import (
email_settings,
email_templates,
media,
messages,
notifications,
)
# Create vendor router
router = APIRouter()
# ============================================================================
# JSON API ROUTES ONLY
# ============================================================================
# These routes return JSON and are mounted at /api/v1/vendor/*
# Email configuration
router.include_router(email_templates.router, tags=["vendor-email-templates"])
router.include_router(email_settings.router, tags=["vendor-email-settings"])
# Services (with prefixes: /media/*, etc.)
router.include_router(media.router, tags=["vendor-media"])
router.include_router(notifications.router, tags=["vendor-notifications"])
router.include_router(messages.router, tags=["vendor-messages"])
# Services (with prefixes: /media/*, etc.)
router.include_router(media.router, tags=["vendor-media"])
router.include_router(notifications.router, tags=["vendor-notifications"])
router.include_router(messages.router, tags=["vendor-messages"])
# All vendor routes are now auto-discovered from self-contained modules.
# ============================================================================

View File

@@ -1,278 +1,25 @@
# app/modules/cms/routes/api/vendor.py
"""
Vendor Content Pages API
CMS module vendor API routes.
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.
Vendors can:
- View their content pages (includes platform defaults)
- Create/edit/delete their own content page overrides
- Preview pages before publishing
Aggregates all vendor CMS routes:
- /content-pages/* - Content page management
- /media/* - Media library management
"""
import logging
from fastapi import APIRouter
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, get_db
from app.modules.cms.exceptions import ContentPageNotFoundException
from app.modules.cms.schemas import (
VendorContentPageCreate,
VendorContentPageUpdate,
ContentPageResponse,
CMSUsageResponse,
)
from app.modules.cms.services import content_page_service
from app.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
from models.database.user import User
from .vendor_content_pages import vendor_content_pages_router
from .vendor_media import vendor_media_router
# Route configuration for auto-discovery
ROUTE_CONFIG = {
"prefix": "/content-pages",
"tags": ["vendor-content-pages"],
"priority": 100, # Register last (CMS has catch-all slug routes)
}
vendor_service = VendorService()
vendor_router = APIRouter()
router = vendor_router # Alias for discovery compatibility
router = APIRouter()
vendor_router = router # Alias for discovery compatibility
logger = logging.getLogger(__name__)
# ============================================================================
# VENDOR CONTENT PAGES
# ============================================================================
@router.get("/", response_model=list[ContentPageResponse])
def list_vendor_pages(
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
List all content pages available for this vendor.
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
"""
pages = content_page_service.list_pages_for_vendor(
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@router.get("/overrides", response_model=list[ContentPageResponse])
def list_vendor_overrides(
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
List only vendor-specific content pages (no platform defaults).
Shows what the vendor has customized.
"""
pages = content_page_service.list_all_vendor_pages(
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@router.get("/usage", response_model=CMSUsageResponse)
def get_cms_usage(
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get CMS usage statistics for the vendor.
Returns page counts and limits based on subscription tier.
"""
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
if not vendor:
return CMSUsageResponse(
total_pages=0,
custom_pages=0,
override_pages=0,
pages_limit=3,
custom_pages_limit=0,
can_create_page=False,
can_create_custom=False,
usage_percent=0,
custom_usage_percent=0,
)
# Get vendor's pages
vendor_pages = content_page_service.list_all_vendor_pages(
db, vendor_id=current_user.token_vendor_id, include_unpublished=True
)
total_pages = len(vendor_pages)
override_pages = sum(1 for p in vendor_pages if p.is_vendor_override)
custom_pages = total_pages - override_pages
# Get limits from subscription tier
pages_limit = None
custom_pages_limit = None
if vendor.subscription and vendor.subscription.tier:
pages_limit = vendor.subscription.tier.cms_pages_limit
custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit
# Calculate can_create flags
can_create_page = pages_limit is None or total_pages < pages_limit
can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit
# Calculate usage percentages
usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100
custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100
return CMSUsageResponse(
total_pages=total_pages,
custom_pages=custom_pages,
override_pages=override_pages,
pages_limit=pages_limit,
custom_pages_limit=custom_pages_limit,
can_create_page=can_create_page,
can_create_custom=can_create_custom,
usage_percent=usage_percent,
custom_usage_percent=custom_usage_percent,
)
@router.get("/platform-default/{slug}", response_model=ContentPageResponse)
def get_platform_default(
slug: str,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get the platform default content for a slug.
Useful for vendors to view the original before/after overriding.
"""
# Get vendor's platform
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
platform_id = 1 # Default to OMS
if vendor and vendor.platforms:
platform_id = vendor.platforms[0].id
# Get platform default (vendor_id=None)
page = content_page_service.get_vendor_default_page(
db, platform_id=platform_id, slug=slug, include_unpublished=True
)
if not page:
raise ContentPageNotFoundException(slug)
return page.to_dict()
@router.get("/{slug}", response_model=ContentPageResponse)
def get_page(
slug: str,
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get a specific content page by slug.
Returns vendor override if exists, otherwise platform default.
"""
page = content_page_service.get_page_for_vendor_or_raise(
db,
slug=slug,
vendor_id=current_user.token_vendor_id,
include_unpublished=include_unpublished,
)
return page.to_dict()
@router.post("/", response_model=ContentPageResponse, status_code=201)
def create_vendor_page(
page_data: VendorContentPageCreate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Create a vendor-specific content page override.
This will be shown instead of the platform default for this vendor.
"""
page = content_page_service.create_page(
db,
slug=page_data.slug,
title=page_data.title,
content=page_data.content,
vendor_id=current_user.token_vendor_id,
content_format=page_data.content_format,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
show_in_legal=page_data.show_in_legal,
display_order=page_data.display_order,
created_by=current_user.id,
)
db.commit()
return page.to_dict()
@router.put("/{page_id}", response_model=ContentPageResponse)
def update_vendor_page(
page_id: int,
page_data: VendorContentPageUpdate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update a vendor-specific content page.
Can only update pages owned by this vendor.
"""
# Update with ownership check in service layer
page = content_page_service.update_vendor_page(
db,
page_id=page_id,
vendor_id=current_user.token_vendor_id,
title=page_data.title,
content=page_data.content,
content_format=page_data.content_format,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
show_in_legal=page_data.show_in_legal,
display_order=page_data.display_order,
updated_by=current_user.id,
)
db.commit()
return page.to_dict()
@router.delete("/{page_id}", status_code=204)
def delete_vendor_page(
page_id: int,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Delete a vendor-specific content page.
Can only delete pages owned by this vendor.
After deletion, platform default will be shown (if exists).
"""
# Delete with ownership check in service layer
content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id)
db.commit()
# Aggregate all CMS vendor routes
vendor_router.include_router(vendor_content_pages_router, tags=["vendor-content-pages"])
vendor_router.include_router(vendor_media_router, tags=["vendor-media"])

View File

@@ -0,0 +1,270 @@
# app/modules/cms/routes/api/vendor_content_pages.py
"""
Vendor Content Pages API
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.
Vendors can:
- View their content pages (includes platform defaults)
- Create/edit/delete their own content page overrides
- Preview pages before publishing
"""
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, get_db
from app.modules.cms.exceptions import ContentPageNotFoundException
from app.modules.cms.schemas import (
VendorContentPageCreate,
VendorContentPageUpdate,
ContentPageResponse,
CMSUsageResponse,
)
from app.modules.cms.services import content_page_service
from app.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
from models.database.user import User
vendor_service = VendorService()
vendor_content_pages_router = APIRouter(prefix="/content-pages")
logger = logging.getLogger(__name__)
# ============================================================================
# VENDOR CONTENT PAGES
# ============================================================================
@vendor_content_pages_router.get("/", response_model=list[ContentPageResponse])
def list_vendor_pages(
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
List all content pages available for this vendor.
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
"""
pages = content_page_service.list_pages_for_vendor(
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@vendor_content_pages_router.get("/overrides", response_model=list[ContentPageResponse])
def list_vendor_overrides(
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
List only vendor-specific content pages (no platform defaults).
Shows what the vendor has customized.
"""
pages = content_page_service.list_all_vendor_pages(
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@vendor_content_pages_router.get("/usage", response_model=CMSUsageResponse)
def get_cms_usage(
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get CMS usage statistics for the vendor.
Returns page counts and limits based on subscription tier.
"""
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
if not vendor:
return CMSUsageResponse(
total_pages=0,
custom_pages=0,
override_pages=0,
pages_limit=3,
custom_pages_limit=0,
can_create_page=False,
can_create_custom=False,
usage_percent=0,
custom_usage_percent=0,
)
# Get vendor's pages
vendor_pages = content_page_service.list_all_vendor_pages(
db, vendor_id=current_user.token_vendor_id, include_unpublished=True
)
total_pages = len(vendor_pages)
override_pages = sum(1 for p in vendor_pages if p.is_vendor_override)
custom_pages = total_pages - override_pages
# Get limits from subscription tier
pages_limit = None
custom_pages_limit = None
if vendor.subscription and vendor.subscription.tier:
pages_limit = vendor.subscription.tier.cms_pages_limit
custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit
# Calculate can_create flags
can_create_page = pages_limit is None or total_pages < pages_limit
can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit
# Calculate usage percentages
usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100
custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100
return CMSUsageResponse(
total_pages=total_pages,
custom_pages=custom_pages,
override_pages=override_pages,
pages_limit=pages_limit,
custom_pages_limit=custom_pages_limit,
can_create_page=can_create_page,
can_create_custom=can_create_custom,
usage_percent=usage_percent,
custom_usage_percent=custom_usage_percent,
)
@vendor_content_pages_router.get("/platform-default/{slug}", response_model=ContentPageResponse)
def get_platform_default(
slug: str,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get the platform default content for a slug.
Useful for vendors to view the original before/after overriding.
"""
# Get vendor's platform
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
platform_id = 1 # Default to OMS
if vendor and vendor.platforms:
platform_id = vendor.platforms[0].id
# Get platform default (vendor_id=None)
page = content_page_service.get_vendor_default_page(
db, platform_id=platform_id, slug=slug, include_unpublished=True
)
if not page:
raise ContentPageNotFoundException(slug)
return page.to_dict()
@vendor_content_pages_router.get("/{slug}", response_model=ContentPageResponse)
def get_page(
slug: str,
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get a specific content page by slug.
Returns vendor override if exists, otherwise platform default.
"""
page = content_page_service.get_page_for_vendor_or_raise(
db,
slug=slug,
vendor_id=current_user.token_vendor_id,
include_unpublished=include_unpublished,
)
return page.to_dict()
@vendor_content_pages_router.post("/", response_model=ContentPageResponse, status_code=201)
def create_vendor_page(
page_data: VendorContentPageCreate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Create a vendor-specific content page override.
This will be shown instead of the platform default for this vendor.
"""
page = content_page_service.create_page(
db,
slug=page_data.slug,
title=page_data.title,
content=page_data.content,
vendor_id=current_user.token_vendor_id,
content_format=page_data.content_format,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
show_in_legal=page_data.show_in_legal,
display_order=page_data.display_order,
created_by=current_user.id,
)
db.commit()
return page.to_dict()
@vendor_content_pages_router.put("/{page_id}", response_model=ContentPageResponse)
def update_vendor_page(
page_id: int,
page_data: VendorContentPageUpdate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update a vendor-specific content page.
Can only update pages owned by this vendor.
"""
# Update with ownership check in service layer
page = content_page_service.update_vendor_page(
db,
page_id=page_id,
vendor_id=current_user.token_vendor_id,
title=page_data.title,
content=page_data.content,
content_format=page_data.content_format,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
show_in_legal=page_data.show_in_legal,
display_order=page_data.display_order,
updated_by=current_user.id,
)
db.commit()
return page.to_dict()
@vendor_content_pages_router.delete("/{page_id}", status_code=204)
def delete_vendor_page(
page_id: int,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Delete a vendor-specific content page.
Can only delete pages owned by this vendor.
After deletion, platform default will be shown (if exists).
"""
# Delete with ownership check in service layer
content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id)
db.commit()

View File

@@ -1,4 +1,4 @@
# app/api/v1/vendor/media.py
# app/modules/cms/routes/api/vendor_media.py
"""
Vendor media and file management endpoints.
@@ -29,11 +29,11 @@ from models.schema.media import (
FailedFileInfo,
)
router = APIRouter(prefix="/media")
vendor_media_router = APIRouter(prefix="/media")
logger = logging.getLogger(__name__)
@router.get("", response_model=MediaListResponse)
@vendor_media_router.get("", response_model=MediaListResponse)
def get_media_library(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
@@ -70,7 +70,7 @@ def get_media_library(
)
@router.post("/upload", response_model=MediaUploadResponse)
@vendor_media_router.post("/upload", response_model=MediaUploadResponse)
async def upload_media(
file: UploadFile = File(...),
folder: str | None = Query("general", description="products, general, etc."),
@@ -112,7 +112,7 @@ async def upload_media(
)
@router.post("/upload/multiple", response_model=MultipleUploadResponse)
@vendor_media_router.post("/upload/multiple", response_model=MultipleUploadResponse)
async def upload_multiple_media(
files: list[UploadFile] = File(...),
folder: str | None = Query("general"),
@@ -167,7 +167,7 @@ async def upload_multiple_media(
)
@router.get("/{media_id}", response_model=MediaDetailResponse)
@vendor_media_router.get("/{media_id}", response_model=MediaDetailResponse)
def get_media_details(
media_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -190,7 +190,7 @@ def get_media_details(
return MediaDetailResponse.model_validate(media)
@router.put("/{media_id}", response_model=MediaDetailResponse)
@vendor_media_router.put("/{media_id}", response_model=MediaDetailResponse)
def update_media_metadata(
media_id: int,
metadata: MediaMetadataUpdate,
@@ -222,7 +222,7 @@ def update_media_metadata(
return MediaDetailResponse.model_validate(media)
@router.delete("/{media_id}", response_model=MediaDetailResponse)
@vendor_media_router.delete("/{media_id}", response_model=MediaDetailResponse)
def delete_media(
media_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -248,7 +248,7 @@ def delete_media(
return MediaDetailResponse(message="Media file deleted successfully")
@router.get("/{media_id}/usage", response_model=MediaUsageResponse)
@vendor_media_router.get("/{media_id}/usage", response_model=MediaUsageResponse)
def get_media_usage(
media_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -270,7 +270,7 @@ def get_media_usage(
return MediaUsageResponse(**usage)
@router.post("/optimize/{media_id}", response_model=OptimizationResultResponse)
@vendor_media_router.post("/optimize/{media_id}", response_model=OptimizationResultResponse)
def optimize_media(
media_id: int,
current_user: UserContext = Depends(get_current_vendor_api),

View File

@@ -2,38 +2,15 @@
"""
CMS module vendor routes.
This module wraps the existing vendor content pages and media routes
and adds module-based access control. Routes are re-exported from the
original location with the module access dependency.
Re-exports routes from the API routes for backwards compatibility
with the lazy router attachment pattern.
Includes:
- /content-pages/* - Content page management
- /media/* - Media library
"""
from fastapi import APIRouter, Depends
# Re-export vendor_router from API routes
from app.modules.cms.routes.api.vendor import vendor_router
from app.api.deps import require_module_access
# Import original routers (direct import to avoid circular dependency)
from app.api.v1.vendor.content_pages import router as content_original_router
from app.api.v1.vendor.media import router as media_original_router
# Create module-aware router for content pages
vendor_router = APIRouter(
prefix="/content-pages",
dependencies=[Depends(require_module_access("cms"))],
)
# Re-export all routes from the original content pages module
for route in content_original_router.routes:
vendor_router.routes.append(route)
# Create separate router for media library
vendor_media_router = APIRouter(
prefix="/media",
dependencies=[Depends(require_module_access("cms"))],
)
for route in media_original_router.routes:
vendor_media_router.routes.append(route)
__all__ = ["vendor_router"]

View File

@@ -1,9 +1,21 @@
# app/modules/messaging/routes/api/__init__.py
"""Messaging module API routes."""
"""
Messaging module API routes.
Vendor routes:
- /messages/* - Conversation and message management
- /notifications/* - Vendor notifications
- /email-settings/* - SMTP and provider configuration
- /email-templates/* - Email template customization
Storefront routes:
- Customer-facing messaging
"""
from app.modules.messaging.routes.api.storefront import router as storefront_router
from app.modules.messaging.routes.api.vendor import vendor_router
# Tag for OpenAPI documentation
STOREFRONT_TAG = "Messages (Storefront)"
__all__ = ["storefront_router", "STOREFRONT_TAG"]
__all__ = ["storefront_router", "vendor_router", "STOREFRONT_TAG"]

View File

@@ -0,0 +1,29 @@
# app/modules/messaging/routes/api/vendor.py
"""
Messaging module vendor API routes.
Aggregates all vendor messaging routes:
- /messages/* - Conversation and message management
- /notifications/* - Vendor notifications
- /email-settings/* - SMTP and provider configuration
- /email-templates/* - Email template customization
"""
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from .vendor_messages import vendor_messages_router
from .vendor_notifications import vendor_notifications_router
from .vendor_email_settings import vendor_email_settings_router
from .vendor_email_templates import vendor_email_templates_router
vendor_router = APIRouter(
dependencies=[Depends(require_module_access("messaging"))],
)
# Aggregate all messaging vendor routes
vendor_router.include_router(vendor_messages_router, tags=["vendor-messages"])
vendor_router.include_router(vendor_notifications_router, tags=["vendor-notifications"])
vendor_router.include_router(vendor_email_settings_router, tags=["vendor-email-settings"])
vendor_router.include_router(vendor_email_templates_router, tags=["vendor-email-templates"])

View File

@@ -1,4 +1,4 @@
# app/api/v1/vendor/email_settings.py
# app/modules/messaging/routes/api/vendor_email_settings.py
"""
Vendor email settings API endpoints.
@@ -24,7 +24,7 @@ from app.services.vendor_email_settings_service import VendorEmailSettingsServic
from app.services.subscription_service import subscription_service
from models.schema.auth import UserContext
router = APIRouter(prefix="/email-settings")
vendor_email_settings_router = APIRouter(prefix="/email-settings")
logger = logging.getLogger(__name__)
@@ -126,7 +126,7 @@ class EmailDeleteResponse(BaseModel):
# =============================================================================
@router.get("", response_model=EmailSettingsResponse)
@vendor_email_settings_router.get("", response_model=EmailSettingsResponse)
def get_email_settings(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -154,7 +154,7 @@ def get_email_settings(
)
@router.get("/status", response_model=EmailStatusResponse)
@vendor_email_settings_router.get("/status", response_model=EmailStatusResponse)
def get_email_status(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -170,7 +170,7 @@ def get_email_status(
return EmailStatusResponse(**status)
@router.get("/providers", response_model=ProvidersResponse)
@vendor_email_settings_router.get("/providers", response_model=ProvidersResponse)
def get_available_providers(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -192,7 +192,7 @@ def get_available_providers(
)
@router.put("", response_model=EmailUpdateResponse)
@vendor_email_settings_router.put("", response_model=EmailUpdateResponse)
def update_email_settings(
data: EmailSettingsUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -226,7 +226,7 @@ def update_email_settings(
)
@router.post("/verify", response_model=EmailVerifyResponse)
@vendor_email_settings_router.post("/verify", response_model=EmailVerifyResponse)
def verify_email_settings(
data: VerifyEmailRequest,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -252,7 +252,7 @@ def verify_email_settings(
)
@router.delete("", response_model=EmailDeleteResponse)
@vendor_email_settings_router.delete("", response_model=EmailDeleteResponse)
def delete_email_settings(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),

View File

@@ -1,4 +1,4 @@
# app/api/v1/vendor/email_templates.py
# app/modules/messaging/routes/api/vendor_email_templates.py
"""
Vendor email template override endpoints.
@@ -22,7 +22,7 @@ from app.services.email_template_service import EmailTemplateService
from app.services.vendor_service import vendor_service
from models.schema.auth import UserContext
router = APIRouter(prefix="/email-templates")
vendor_email_templates_router = APIRouter(prefix="/email-templates")
logger = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ class TemplateTestRequest(BaseModel):
# =============================================================================
@router.get("")
@vendor_email_templates_router.get("")
def list_overridable_templates(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -76,7 +76,7 @@ def list_overridable_templates(
return service.list_overridable_templates(vendor_id)
@router.get("/{code}")
@vendor_email_templates_router.get("/{code}")
def get_template(
code: str,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -92,7 +92,7 @@ def get_template(
return service.get_vendor_template(vendor_id, code)
@router.get("/{code}/{language}")
@vendor_email_templates_router.get("/{code}/{language}")
def get_template_language(
code: str,
language: str,
@@ -109,7 +109,7 @@ def get_template_language(
return service.get_vendor_template_language(vendor_id, code, language)
@router.put("/{code}/{language}")
@vendor_email_templates_router.put("/{code}/{language}")
def update_template_override(
code: str,
language: str,
@@ -140,7 +140,7 @@ def update_template_override(
return result
@router.delete("/{code}/{language}")
@vendor_email_templates_router.delete("/{code}/{language}")
def delete_template_override(
code: str,
language: str,
@@ -164,7 +164,7 @@ def delete_template_override(
}
@router.post("/{code}/preview")
@vendor_email_templates_router.post("/{code}/preview")
def preview_template(
code: str,
preview_data: TemplatePreviewRequest,
@@ -197,7 +197,7 @@ def preview_template(
)
@router.post("/{code}/test")
@vendor_email_templates_router.post("/{code}/test")
def send_test_email(
code: str,
test_data: TemplateTestRequest,

View File

@@ -1,4 +1,4 @@
# app/api/v1/vendor/messages.py
# app/modules/messaging/routes/api/vendor_messages.py
"""
Vendor messaging endpoints.
@@ -49,7 +49,7 @@ from app.modules.messaging.schemas import (
)
from models.schema.auth import UserContext
router = APIRouter(prefix="/messages")
vendor_messages_router = APIRouter(prefix="/messages")
logger = logging.getLogger(__name__)
@@ -170,7 +170,7 @@ def _enrich_conversation_summary(
# ============================================================================
@router.get("", response_model=ConversationListResponse)
@vendor_messages_router.get("", response_model=ConversationListResponse)
def list_conversations(
conversation_type: ConversationType | None = Query(None, description="Filter by type"),
is_closed: bool | None = Query(None, description="Filter by status"),
@@ -205,7 +205,7 @@ def list_conversations(
)
@router.get("/unread-count", response_model=UnreadCountResponse)
@vendor_messages_router.get("/unread-count", response_model=UnreadCountResponse)
def get_unread_count(
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_vendor_api),
@@ -227,7 +227,7 @@ def get_unread_count(
# ============================================================================
@router.get("/recipients", response_model=RecipientListResponse)
@vendor_messages_router.get("/recipients", response_model=RecipientListResponse)
def get_recipients(
recipient_type: ParticipantType = Query(..., description="Type of recipients to list"),
search: str | None = Query(None, description="Search by name/email"),
@@ -271,7 +271,7 @@ def get_recipients(
# ============================================================================
@router.post("", response_model=ConversationDetailResponse)
@vendor_messages_router.post("", response_model=ConversationDetailResponse)
def create_conversation(
data: ConversationCreate,
db: Session = Depends(get_db),
@@ -394,7 +394,7 @@ def _build_conversation_detail(
)
@router.get("/{conversation_id}", response_model=ConversationDetailResponse)
@vendor_messages_router.get("/{conversation_id}", response_model=ConversationDetailResponse)
def get_conversation(
conversation_id: int,
mark_read: bool = Query(True, description="Automatically mark as read"),
@@ -436,7 +436,7 @@ def get_conversation(
# ============================================================================
@router.post("/{conversation_id}/messages", response_model=MessageResponse)
@vendor_messages_router.post("/{conversation_id}/messages", response_model=MessageResponse)
async def send_message(
conversation_id: int,
content: str = Form(...),
@@ -501,7 +501,7 @@ async def send_message(
# ============================================================================
@router.post("/{conversation_id}/close", response_model=CloseConversationResponse)
@vendor_messages_router.post("/{conversation_id}/close", response_model=CloseConversationResponse)
def close_conversation(
conversation_id: int,
db: Session = Depends(get_db),
@@ -543,7 +543,7 @@ def close_conversation(
)
@router.post("/{conversation_id}/reopen", response_model=ReopenConversationResponse)
@vendor_messages_router.post("/{conversation_id}/reopen", response_model=ReopenConversationResponse)
def reopen_conversation(
conversation_id: int,
db: Session = Depends(get_db),
@@ -585,7 +585,7 @@ def reopen_conversation(
)
@router.put("/{conversation_id}/read", response_model=MarkReadResponse)
@vendor_messages_router.put("/{conversation_id}/read", response_model=MarkReadResponse)
def mark_read(
conversation_id: int,
db: Session = Depends(get_db),
@@ -612,7 +612,7 @@ class PreferencesUpdateResponse(BaseModel):
success: bool
@router.put("/{conversation_id}/preferences", response_model=PreferencesUpdateResponse)
@vendor_messages_router.put("/{conversation_id}/preferences", response_model=PreferencesUpdateResponse)
def update_preferences(
conversation_id: int,
preferences: NotificationPreferencesUpdate,

View File

@@ -1,4 +1,4 @@
# app/api/v1/vendor/notifications.py
# app/modules/messaging/routes/api/vendor_notifications.py
"""
Vendor notification management endpoints.
@@ -26,11 +26,11 @@ from app.modules.messaging.schemas import (
UnreadCountResponse,
)
router = APIRouter(prefix="/notifications")
vendor_notifications_router = APIRouter(prefix="/notifications")
logger = logging.getLogger(__name__)
@router.get("", response_model=NotificationListResponse)
@vendor_notifications_router.get("", response_model=NotificationListResponse)
def get_notifications(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
@@ -56,7 +56,7 @@ def get_notifications(
)
@router.get("/unread-count", response_model=UnreadCountResponse)
@vendor_notifications_router.get("/unread-count", response_model=UnreadCountResponse)
def get_unread_count(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -72,7 +72,7 @@ def get_unread_count(
return UnreadCountResponse(unread_count=0, message="Unread count coming in Slice 5")
@router.put("/{notification_id}/read", response_model=MessageResponse)
@vendor_notifications_router.put("/{notification_id}/read", response_model=MessageResponse)
def mark_as_read(
notification_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -89,7 +89,7 @@ def mark_as_read(
return MessageResponse(message="Mark as read coming in Slice 5")
@router.put("/mark-all-read", response_model=MessageResponse)
@vendor_notifications_router.put("/mark-all-read", response_model=MessageResponse)
def mark_all_as_read(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -105,7 +105,7 @@ def mark_all_as_read(
return MessageResponse(message="Mark all as read coming in Slice 5")
@router.delete("/{notification_id}", response_model=MessageResponse)
@vendor_notifications_router.delete("/{notification_id}", response_model=MessageResponse)
def delete_notification(
notification_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -122,7 +122,7 @@ def delete_notification(
return MessageResponse(message="Notification deletion coming in Slice 5")
@router.get("/settings", response_model=NotificationSettingsResponse)
@vendor_notifications_router.get("/settings", response_model=NotificationSettingsResponse)
def get_notification_settings(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -144,7 +144,7 @@ def get_notification_settings(
)
@router.put("/settings", response_model=MessageResponse)
@vendor_notifications_router.put("/settings", response_model=MessageResponse)
def update_notification_settings(
settings: NotificationSettingsUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
@@ -162,7 +162,7 @@ def update_notification_settings(
return MessageResponse(message="Notification settings update coming in Slice 5")
@router.get("/templates", response_model=NotificationTemplateListResponse)
@vendor_notifications_router.get("/templates", response_model=NotificationTemplateListResponse)
def get_notification_templates(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
@@ -181,7 +181,7 @@ def get_notification_templates(
)
@router.put("/templates/{template_id}", response_model=MessageResponse)
@vendor_notifications_router.put("/templates/{template_id}", response_model=MessageResponse)
def update_notification_template(
template_id: int,
template_data: NotificationTemplateUpdate,
@@ -201,7 +201,7 @@ def update_notification_template(
return MessageResponse(message="Template update coming in Slice 5")
@router.post("/test", response_model=MessageResponse)
@vendor_notifications_router.post("/test", response_model=MessageResponse)
def send_test_notification(
notification_data: TestNotificationRequest,
current_user: UserContext = Depends(get_current_vendor_api),

View File

@@ -1,39 +0,0 @@
# app/modules/messaging/routes/vendor.py
"""
Messaging module vendor routes.
This module wraps the existing vendor messages and notifications routes
and adds module-based access control. Routes are re-exported from the
original location with the module access dependency.
Includes:
- /messages/* - Message management
- /notifications/* - Notification management
"""
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
# Import original routers (direct import to avoid circular dependency)
from app.api.v1.vendor.messages import router as messages_original_router
from app.api.v1.vendor.notifications import router as notifications_original_router
# Create module-aware router for messages
vendor_router = APIRouter(
prefix="/messages",
dependencies=[Depends(require_module_access("messaging"))],
)
# Re-export all routes from the original messages module
for route in messages_original_router.routes:
vendor_router.routes.append(route)
# Create separate router for notifications
vendor_notifications_router = APIRouter(
prefix="/notifications",
dependencies=[Depends(require_module_access("messaging"))],
)
for route in notifications_original_router.routes:
vendor_notifications_router.routes.append(route)