From 7639ca602b833caba2ad76b151b554e97fd409c7 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 31 Jan 2026 15:21:25 +0100 Subject: [PATCH] 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 --- app/api/v1/vendor/__init__.py | 29 +- app/modules/cms/routes/api/vendor.py | 277 +----------------- .../cms/routes/api/vendor_content_pages.py | 270 +++++++++++++++++ .../cms/routes/api/vendor_media.py} | 20 +- app/modules/cms/routes/vendor.py | 33 +-- app/modules/messaging/routes/api/__init__.py | 16 +- app/modules/messaging/routes/api/vendor.py | 29 ++ .../routes/api/vendor_email_settings.py} | 16 +- .../routes/api/vendor_email_templates.py} | 18 +- .../messaging/routes/api/vendor_messages.py} | 24 +- .../routes/api/vendor_notifications.py} | 24 +- app/modules/messaging/routes/vendor.py | 39 --- 12 files changed, 385 insertions(+), 410 deletions(-) create mode 100644 app/modules/cms/routes/api/vendor_content_pages.py rename app/{api/v1/vendor/media.py => modules/cms/routes/api/vendor_media.py} (91%) create mode 100644 app/modules/messaging/routes/api/vendor.py rename app/{api/v1/vendor/email_settings.py => modules/messaging/routes/api/vendor_email_settings.py} (93%) rename app/{api/v1/vendor/email_templates.py => modules/messaging/routes/api/vendor_email_templates.py} (94%) rename app/{api/v1/vendor/messages.py => modules/messaging/routes/api/vendor_messages.py} (95%) rename app/{api/v1/vendor/notifications.py => modules/messaging/routes/api/vendor_notifications.py} (86%) delete mode 100644 app/modules/messaging/routes/vendor.py diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index 2eca6cfd..cd2eeb5a 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -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. # ============================================================================ diff --git a/app/modules/cms/routes/api/vendor.py b/app/modules/cms/routes/api/vendor.py index 2d3113f3..5b581b42 100644 --- a/app/modules/cms/routes/api/vendor.py +++ b/app/modules/cms/routes/api/vendor.py @@ -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"]) diff --git a/app/modules/cms/routes/api/vendor_content_pages.py b/app/modules/cms/routes/api/vendor_content_pages.py new file mode 100644 index 00000000..92040efc --- /dev/null +++ b/app/modules/cms/routes/api/vendor_content_pages.py @@ -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() diff --git a/app/api/v1/vendor/media.py b/app/modules/cms/routes/api/vendor_media.py similarity index 91% rename from app/api/v1/vendor/media.py rename to app/modules/cms/routes/api/vendor_media.py index d5f729c2..377c6a9a 100644 --- a/app/api/v1/vendor/media.py +++ b/app/modules/cms/routes/api/vendor_media.py @@ -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), diff --git a/app/modules/cms/routes/vendor.py b/app/modules/cms/routes/vendor.py index fd7b33d9..b4b9c04a 100644 --- a/app/modules/cms/routes/vendor.py +++ b/app/modules/cms/routes/vendor.py @@ -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"] diff --git a/app/modules/messaging/routes/api/__init__.py b/app/modules/messaging/routes/api/__init__.py index c2cec261..c582707d 100644 --- a/app/modules/messaging/routes/api/__init__.py +++ b/app/modules/messaging/routes/api/__init__.py @@ -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"] diff --git a/app/modules/messaging/routes/api/vendor.py b/app/modules/messaging/routes/api/vendor.py new file mode 100644 index 00000000..eebd7d5e --- /dev/null +++ b/app/modules/messaging/routes/api/vendor.py @@ -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"]) diff --git a/app/api/v1/vendor/email_settings.py b/app/modules/messaging/routes/api/vendor_email_settings.py similarity index 93% rename from app/api/v1/vendor/email_settings.py rename to app/modules/messaging/routes/api/vendor_email_settings.py index c5f0112d..beec31bb 100644 --- a/app/api/v1/vendor/email_settings.py +++ b/app/modules/messaging/routes/api/vendor_email_settings.py @@ -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), diff --git a/app/api/v1/vendor/email_templates.py b/app/modules/messaging/routes/api/vendor_email_templates.py similarity index 94% rename from app/api/v1/vendor/email_templates.py rename to app/modules/messaging/routes/api/vendor_email_templates.py index f7860d5b..0af17d99 100644 --- a/app/api/v1/vendor/email_templates.py +++ b/app/modules/messaging/routes/api/vendor_email_templates.py @@ -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, diff --git a/app/api/v1/vendor/messages.py b/app/modules/messaging/routes/api/vendor_messages.py similarity index 95% rename from app/api/v1/vendor/messages.py rename to app/modules/messaging/routes/api/vendor_messages.py index 0051e7a6..40c48c65 100644 --- a/app/api/v1/vendor/messages.py +++ b/app/modules/messaging/routes/api/vendor_messages.py @@ -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, diff --git a/app/api/v1/vendor/notifications.py b/app/modules/messaging/routes/api/vendor_notifications.py similarity index 86% rename from app/api/v1/vendor/notifications.py rename to app/modules/messaging/routes/api/vendor_notifications.py index 2d4ba26e..9f12fa89 100644 --- a/app/api/v1/vendor/notifications.py +++ b/app/modules/messaging/routes/api/vendor_notifications.py @@ -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), diff --git a/app/modules/messaging/routes/vendor.py b/app/modules/messaging/routes/vendor.py deleted file mode 100644 index 9766b24d..00000000 --- a/app/modules/messaging/routes/vendor.py +++ /dev/null @@ -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)