diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index 98cac31b..5b9ac4f0 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -2,12 +2,19 @@ """ Vendor API endpoints. -This module aggregates all vendor-related endpoints with proper prefixes. +This module aggregates all vendor-related JSON API endpoints. + +IMPORTANT: +- This router is for JSON API endpoints only +- HTML page routes are mounted separately in main.py at /vendor/* +- Do NOT include pages.router here - it causes route conflicts """ from fastapi import APIRouter -# Import all sub-routers + +# Import all sub-routers (JSON API only) from . import ( + info, # NEW: Vendor info endpoint auth, dashboard, profile, @@ -22,28 +29,51 @@ from . import ( media, notifications, analytics, + # NOTE: pages is NOT imported here - it's mounted separately in main.py ) # Create vendor router router = APIRouter() -# Include all vendor sub-routers with their prefixes and tags -# Note: prefixes are already defined in each router file +# ============================================================================ +# JSON API ROUTES ONLY +# ============================================================================ +# These routes return JSON and are mounted at /api/v1/vendor/* +# Vendor info endpoint - must be first to handle GET /{vendor_code} +router.include_router(info.router, tags=["vendor-info"]) + +# Authentication router.include_router(auth.router, tags=["vendor-auth"]) + +# Vendor management router.include_router(dashboard.router, tags=["vendor-dashboard"]) router.include_router(profile.router, tags=["vendor-profile"]) router.include_router(settings.router, tags=["vendor-settings"]) + +# Business operations router.include_router(products.router, tags=["vendor-products"]) router.include_router(orders.router, tags=["vendor-orders"]) router.include_router(customers.router, tags=["vendor-customers"]) router.include_router(teams.router, tags=["vendor-teams"]) router.include_router(inventory.router, tags=["vendor-inventory"]) router.include_router(marketplace.router, tags=["vendor-marketplace"]) + +# Services router.include_router(payments.router, tags=["vendor-payments"]) router.include_router(media.router, tags=["vendor-media"]) router.include_router(notifications.router, tags=["vendor-notifications"]) router.include_router(analytics.router, tags=["vendor-analytics"]) +# ============================================================================ +# NOTE: HTML Page Routes +# ============================================================================ +# HTML page routes (pages.router) are NOT included here. +# They are mounted separately in main.py at /vendor/* to avoid conflicts. +# +# This separation ensures: +# - JSON API: /api/v1/vendor/* (this router) +# - HTML Pages: /vendor/* (mounted in main.py) + __all__ = ["router"] diff --git a/app/api/v1/vendor/info.py b/app/api/v1/vendor/info.py new file mode 100644 index 00000000..fffcebb7 --- /dev/null +++ b/app/api/v1/vendor/info.py @@ -0,0 +1,106 @@ +# app/api/v1/vendor/info.py +""" +Vendor information endpoints. + +This module provides: +- Public vendor information lookup (no auth required) +- Used by vendor login pages to display vendor details +""" + +import logging +from fastapi import APIRouter, Path, Depends +from sqlalchemy.orm import Session +from sqlalchemy import func + +from app.core.database import get_db +from app.exceptions import VendorNotFoundException +from models.schema.vendor import VendorResponse, VendorDetailResponse +from models.database.vendor import Vendor + +router = APIRouter() +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) +def get_vendor_info( + vendor_code: str = Path(..., description="Vendor code"), + db: Session = Depends(get_db) +): + """ + Get public vendor information by vendor code. + + This endpoint is used by the vendor login page to display vendor info. + No authentication required - this is public information. + + **Use Case:** + - Vendor login page loads vendor info to display branding + - Shows vendor name, description, logo, etc. + + **Returns only active vendors** to prevent access to disabled accounts. + + Args: + vendor_code: The vendor's unique code (e.g., 'WIZAMART') + db: Database session + + Returns: + VendorResponse: Public vendor information + + Raises: + VendorNotFoundException (404): Vendor not found or inactive + """ + logger.info(f"Public vendor info request: {vendor_code}") + + vendor = _get_vendor_by_code(db, vendor_code) + + logger.info(f"Vendor info retrieved: {vendor.name} ({vendor.vendor_code})") + + return VendorDetailResponse( + # Vendor fields + id=vendor.id, + vendor_code=vendor.vendor_code, + subdomain=vendor.subdomain, + name=vendor.name, + description=vendor.description, + owner_user_id=vendor.owner_user_id, + contact_email=vendor.contact_email, + contact_phone=vendor.contact_phone, + website=vendor.website, + business_address=vendor.business_address, + tax_number=vendor.tax_number, + 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, + is_active=vendor.is_active, + is_verified=vendor.is_verified, + created_at=vendor.created_at, + updated_at=vendor.updated_at, + # Owner details + owner_email=vendor.owner.email, + owner_username=vendor.owner.username, + ) diff --git a/app/api/v1/vendor/pages.py b/app/api/v1/vendor/pages.py index d38986ee..a13a7e73 100644 --- a/app/api/v1/vendor/pages.py +++ b/app/api/v1/vendor/pages.py @@ -3,7 +3,7 @@ Vendor HTML page routes using Jinja2 templates. These routes serve HTML pages for vendor-facing interfaces. -Supports both path-based (/vendor/{vendor_code}/) and subdomain-based access. +Follows the same minimal server-side rendering pattern as admin routes. All routes except /login require vendor authentication. Authentication failures redirect to /vendor/{vendor_code}/login. @@ -24,9 +24,8 @@ Routes: from fastapi import APIRouter, Request, Depends, Path from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates -from sqlalchemy.orm import Session -from app.api.deps import get_current_vendor_user, get_db +from app.api.deps import get_current_vendor_user from models.database.user import User router = APIRouter() @@ -37,22 +36,37 @@ templates = Jinja2Templates(directory="app/templates") # PUBLIC ROUTES (No Authentication Required) # ============================================================================ -@router.get("/vendor/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False) -async def vendor_root(vendor_code: str = Path(..., description="Vendor code")): +@router.get("/{vendor_code}", response_class=RedirectResponse, include_in_schema=False) +async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor code")): """ - Redirect /vendor/{code}/ to login page. + Redirect /vendor/{code} (no trailing slash) to login page. + Handles requests without trailing slash. """ return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302) -@router.get("/vendor/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False) +async def vendor_root(vendor_code: str = Path(..., description="Vendor code")): + """ + Redirect /vendor/{code}/ to login page. + Simple approach - let login page handle authenticated redirects. + """ + return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302) + + +@router.get("/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False) async def vendor_login_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code") + request: Request, + vendor_code: str = Path(..., description="Vendor code") ): """ Render vendor login page. No authentication required. + + JavaScript will: + - Load vendor info via API + - Handle login form submission + - Redirect to dashboard on success """ return templates.TemplateResponse( "vendor/login.html", @@ -67,19 +81,23 @@ async def vendor_login_page( # AUTHENTICATED ROUTES (Vendor Users Only) # ============================================================================ -@router.get("/vendor/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False) async def vendor_dashboard_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render vendor dashboard. - Shows sales metrics, recent orders, and quick actions. + + JavaScript will: + - Load vendor info via API + - Load dashboard stats via API + - Load recent orders via API + - Handle all interactivity """ return templates.TemplateResponse( - "vendor/dashboard.html", + "vendor/admin/dashboard.html", { "request": request, "user": current_user, @@ -92,17 +110,15 @@ async def vendor_dashboard_page( # PRODUCT MANAGEMENT # ============================================================================ -@router.get("/vendor/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/products", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False) async def vendor_products_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render products management page. - List, create, edit, and manage products. + JavaScript loads product list via API. """ return templates.TemplateResponse( "vendor/admin/products.html", @@ -118,17 +134,15 @@ async def vendor_products_page( # ORDER MANAGEMENT # ============================================================================ -@router.get("/vendor/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/orders", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False) async def vendor_orders_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render orders management page. - View and process orders. + JavaScript loads order list via API. """ return templates.TemplateResponse( "vendor/admin/orders.html", @@ -144,17 +158,15 @@ async def vendor_orders_page( # CUSTOMER MANAGEMENT # ============================================================================ -@router.get("/vendor/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/customers", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False) async def vendor_customers_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render customers management page. - View and manage customer relationships. + JavaScript loads customer list via API. """ return templates.TemplateResponse( "vendor/admin/customers.html", @@ -170,17 +182,15 @@ async def vendor_customers_page( # INVENTORY MANAGEMENT # ============================================================================ -@router.get("/vendor/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/inventory", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False) async def vendor_inventory_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render inventory management page. - Track and manage stock levels. + JavaScript loads inventory data via API. """ return templates.TemplateResponse( "vendor/admin/inventory.html", @@ -196,17 +206,15 @@ async def vendor_inventory_page( # MARKETPLACE IMPORTS # ============================================================================ -@router.get("/vendor/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/marketplace", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False) async def vendor_marketplace_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render marketplace import page. - Import products from external marketplaces. + JavaScript loads import jobs and products via API. """ return templates.TemplateResponse( "vendor/admin/marketplace.html", @@ -222,17 +230,15 @@ async def vendor_marketplace_page( # TEAM MANAGEMENT # ============================================================================ -@router.get("/vendor/{vendor_code}/team", response_class=HTMLResponse, include_in_schema=False) -@router.get("/vendor/{vendor_code}/admin/team", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/team", response_class=HTMLResponse, include_in_schema=False) async def vendor_team_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render team management page. - Manage vendor staff and permissions. + JavaScript loads team members via API. """ return templates.TemplateResponse( "vendor/admin/team.html", @@ -248,16 +254,15 @@ async def vendor_team_page( # SETTINGS # ============================================================================ -@router.get("/vendor/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False) +@router.get("/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False) async def vendor_settings_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) + request: Request, + vendor_code: str = Path(..., description="Vendor code"), + current_user: User = Depends(get_current_vendor_user) ): """ Render vendor settings page. - Configure vendor preferences and integrations. + JavaScript loads settings via API. """ return templates.TemplateResponse( "vendor/settings.html", @@ -267,51 +272,3 @@ async def vendor_settings_page( "vendor_code": vendor_code, } ) - - -# ============================================================================ -# FALLBACK ROUTES (Query Parameter Based - For backward compatibility) -# ============================================================================ - -@router.get("/vendor/", response_class=RedirectResponse, include_in_schema=False) -async def vendor_fallback_root(): - """ - Redirect to main site if no vendor code in path. - User should use /vendor/{code}/ format. - """ - return RedirectResponse(url="/", status_code=302) - - -@router.get("/vendor/login", response_class=HTMLResponse, include_in_schema=False) -async def vendor_fallback_login(request: Request): - """ - Fallback vendor login page (query parameter based). - For backward compatibility - new code should use /vendor/{code}/login - """ - return templates.TemplateResponse( - "vendor/login.html", - { - "request": request, - "vendor_code": None, # Will be retrieved from query param or localStorage - } - ) - - -@router.get("/vendor/dashboard", response_class=HTMLResponse, include_in_schema=False) -async def vendor_fallback_dashboard( - request: Request, - current_user: User = Depends(get_current_vendor_user), - db: Session = Depends(get_db) -): - """ - Fallback vendor dashboard (query parameter based). - For backward compatibility - new code should use /vendor/{code}/dashboard - """ - return templates.TemplateResponse( - "vendor/dashboard.html", - { - "request": request, - "user": current_user, - "vendor_code": None, # Will be retrieved from token or localStorage - } - ) diff --git a/app/exceptions/handler.py b/app/exceptions/handler.py index f208ae01..b54e3719 100644 --- a/app/exceptions/handler.py +++ b/app/exceptions/handler.py @@ -170,6 +170,97 @@ def setup_exception_handlers(app): """Handle all 404 errors with consistent format.""" logger.warning(f"404 Not Found: {request.method} {request.url}") + # Check if this is a browser request (wants HTML) + accept_header = request.headers.get("accept", "") + + # Browser requests typically have "text/html" in accept header + # API requests typically have "application/json" + if "text/html" in accept_header: + # Return simple HTML 404 page for browser + from fastapi.responses import HTMLResponse + + html_content = f""" + + + + + + 404 - Page Not Found + + + +
+

404

+

Page Not Found

+

Sorry, the page you're looking for doesn't exist or has been moved.

+ Go Home +
Path: {request.url.path}
+
+ + + """ + + return HTMLResponse(content=html_content, status_code=404) + + # Return JSON for API requests return JSONResponse( status_code=404, content={ @@ -250,4 +341,4 @@ def raise_auth_error(message: str = "Authentication failed") -> None: def raise_permission_error(message: str = "Access denied") -> None: """Convenience function to raise AuthorizationException.""" from .base import AuthorizationException - raise AuthorizationException(message) \ No newline at end of file + raise AuthorizationException(message) diff --git a/app/exceptions/vendor.py b/app/exceptions/vendor.py index 0b7848e1..5563488f 100644 --- a/app/exceptions/vendor.py +++ b/app/exceptions/vendor.py @@ -79,7 +79,7 @@ class UnauthorizedVendorAccessException(AuthorizationException): class InvalidVendorDataException(ValidationException): - """Raised when vendor data is invalid.""" + """Raised when vendor data is invalid or incomplete.""" def __init__( self, @@ -95,21 +95,6 @@ class InvalidVendorDataException(ValidationException): self.error_code = "INVALID_VENDOR_DATA" -class MaxVendorsReachedException(BusinessLogicException): - """Raised when user tries to create more vendors than allowed.""" - - def __init__(self, max_vendors: int, user_id: Optional[int] = None): - details = {"max_vendors": max_vendors} - if user_id: - details["user_id"] = user_id - - super().__init__( - message=f"Maximum number of vendors reached ({max_vendors})", - error_code="MAX_VENDORS_REACHED", - details=details, - ) - - class VendorValidationException(ValidationException): """Raised when vendor validation fails.""" @@ -129,3 +114,36 @@ class VendorValidationException(ValidationException): details=details, ) self.error_code = "VENDOR_VALIDATION_FAILED" + + +class IncompleteVendorDataException(ValidationException): + """Raised when vendor data is missing required fields.""" + + def __init__( + self, + vendor_code: str, + missing_fields: list, + ): + super().__init__( + message=f"Vendor '{vendor_code}' is missing required fields: {', '.join(missing_fields)}", + details={ + "vendor_code": vendor_code, + "missing_fields": missing_fields, + }, + ) + self.error_code = "INCOMPLETE_VENDOR_DATA" + + +class MaxVendorsReachedException(BusinessLogicException): + """Raised when user tries to create more vendors than allowed.""" + + def __init__(self, max_vendors: int, user_id: Optional[int] = None): + details = {"max_vendors": max_vendors} + if user_id: + details["user_id"] = user_id + + super().__init__( + message=f"Maximum number of vendors reached ({max_vendors})", + error_code="MAX_VENDORS_REACHED", + details=details, + ) diff --git a/app/templates/vendor/base.html b/app/templates/vendor/base.html index 566549bd..90271a19 100644 --- a/app/templates/vendor/base.html +++ b/app/templates/vendor/base.html @@ -1,10 +1,63 @@ +{# app/templates/vendor/base.html #} - + - - Title - - + + + {% block title %}Vendor Panel{% endblock %} - {{ vendor.name if vendor else 'Multi-Tenant Platform' }} + + + + + + + + + + {% block extra_head %}{% endblock %} + + +
+ + {% include 'vendor/partials/sidebar.html' %} + +
+ + {% include 'vendor/partials/header.html' %} + + +
+
+ {% block content %}{% endblock %} +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + {% block extra_scripts %}{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/dashboard.html b/app/templates/vendor/dashboard.html new file mode 100644 index 00000000..a998a5e6 --- /dev/null +++ b/app/templates/vendor/dashboard.html @@ -0,0 +1,168 @@ +{# app/templates/vendor/admin/dashboard.html #} +{% extends "vendor/base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block alpine_data %}vendorDashboard(){% endblock %} + +{% block content %} + +
+

+ Dashboard +

+ +
+ + +
+ +

Loading dashboard...

+
+ + +
+ +
+

Error loading dashboard

+

+
+
+ + +{% include 'vendor/partials/vendor_info.html' %} + + +
+ +
+
+ +
+
+

+ Total Products +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Total Orders +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Total Customers +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Total Revenue +

+

+ โ‚ฌ0 +

+
+
+
+ + +
+
+ + + + + + + + + + + + + +
Order IDCustomerAmountStatusDate
+
+
+ + +
+
+
๐Ÿš€
+

+ Welcome to Your Vendor Dashboard! +

+

+ Start by importing products from the marketplace to build your catalog. +

+ + + Go to Marketplace Import + +
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/login.html b/app/templates/vendor/login.html index 1077f063..9b6c17d8 100644 --- a/app/templates/vendor/login.html +++ b/app/templates/vendor/login.html @@ -1,121 +1,164 @@ +{# app/templates/vendor/login.html #} - + - - - Vendor Login - Multi-Tenant Ecommerce Platform - - - + + + Vendor Login - Multi-Tenant Platform + + + -
-
- - - - - - -
- -
- - -