Fixing vendor dashboard area
This commit is contained in:
16
app/api/v1/vendor/__init__.py
vendored
16
app/api/v1/vendor/__init__.py
vendored
@@ -40,18 +40,19 @@ router = APIRouter()
|
||||
# ============================================================================
|
||||
# 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"])
|
||||
# IMPORTANT: Specific routes MUST be registered BEFORE catch-all routes
|
||||
# The info.router has GET /{vendor_code} which catches everything,
|
||||
# so it must be registered LAST
|
||||
|
||||
# Authentication
|
||||
# Authentication (no prefix, specific routes like /auth/login)
|
||||
router.include_router(auth.router, tags=["vendor-auth"])
|
||||
|
||||
# Vendor management
|
||||
# Vendor management (with prefixes: /dashboard/*, /profile/*, /settings/*)
|
||||
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
|
||||
# Business operations (with prefixes: /products/*, /orders/*, etc.)
|
||||
router.include_router(products.router, tags=["vendor-products"])
|
||||
router.include_router(orders.router, tags=["vendor-orders"])
|
||||
router.include_router(customers.router, tags=["vendor-customers"])
|
||||
@@ -59,10 +60,13 @@ router.include_router(team.router, tags=["vendor-team"])
|
||||
router.include_router(inventory.router, tags=["vendor-inventory"])
|
||||
router.include_router(marketplace.router, tags=["vendor-marketplace"])
|
||||
|
||||
# Services
|
||||
# Services (with prefixes: /payments/*, /media/*, etc.)
|
||||
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"])
|
||||
|
||||
# Vendor info endpoint - MUST BE LAST! Has catch-all GET /{vendor_code}
|
||||
router.include_router(info.router, tags=["vendor-info"])
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
4
app/api/v1/vendor/analytics.py
vendored
4
app/api/v1/vendor/analytics.py
vendored
@@ -7,7 +7,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
def get_vendor_analytics(
|
||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data for specified time period."""
|
||||
|
||||
7
app/api/v1/vendor/auth.py
vendored
7
app/api/v1/vendor/auth.py
vendored
@@ -198,18 +198,15 @@ def vendor_logout(response: Response):
|
||||
|
||||
@router.get("/me")
|
||||
def get_current_vendor_user(
|
||||
request: Request,
|
||||
user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get current authenticated vendor user.
|
||||
|
||||
This endpoint can be called to verify authentication and get user info.
|
||||
Requires Authorization header (header-only authentication for API endpoints).
|
||||
"""
|
||||
|
||||
# This will check both cookie and header
|
||||
user = get_current_vendor_api(request, db=db)
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
|
||||
14
app/api/v1/vendor/customers.py
vendored
14
app/api/v1/vendor/customers.py
vendored
@@ -9,7 +9,7 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
@@ -26,7 +26,7 @@ def get_vendor_customers(
|
||||
search: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -51,7 +51,7 @@ def get_vendor_customers(
|
||||
def get_customer_details(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -72,7 +72,7 @@ def get_customer_details(
|
||||
def get_customer_orders(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -94,7 +94,7 @@ def update_customer(
|
||||
customer_id: int,
|
||||
customer_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -114,7 +114,7 @@ def update_customer(
|
||||
def toggle_customer_status(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -134,7 +134,7 @@ def toggle_customer_status(
|
||||
def get_customer_statistics(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
|
||||
32
app/api/v1/vendor/dashboard.py
vendored
32
app/api/v1/vendor/dashboard.py
vendored
@@ -4,10 +4,10 @@ Vendor dashboard and statistics endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
@@ -20,8 +20,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("/stats")
|
||||
def get_vendor_dashboard_stats(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -32,7 +32,31 @@ def get_vendor_dashboard_stats(
|
||||
- Total orders
|
||||
- Total customers
|
||||
- Revenue metrics
|
||||
|
||||
Vendor is determined from the authenticated user's vendor_user association.
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
# Get vendor from authenticated user's vendor_user record
|
||||
from models.database.vendor import VendorUser
|
||||
vendor_user = db.query(VendorUser).filter(
|
||||
VendorUser.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not vendor_user:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="User is not associated with any vendor"
|
||||
)
|
||||
|
||||
vendor = vendor_user.vendor
|
||||
if not vendor or not vendor.is_active:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found or inactive"
|
||||
)
|
||||
|
||||
# Get vendor-scoped statistics
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor.id)
|
||||
|
||||
|
||||
20
app/api/v1/vendor/inventory.py
vendored
20
app/api/v1/vendor/inventory.py
vendored
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.inventory_service import inventory_service
|
||||
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
|
||||
def set_inventory(
|
||||
inventory: InventoryCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
@@ -40,7 +40,7 @@ def set_inventory(
|
||||
def adjust_inventory(
|
||||
adjustment: InventoryAdjust,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Adjust inventory (positive to add, negative to remove)."""
|
||||
@@ -51,7 +51,7 @@ def adjust_inventory(
|
||||
def reserve_inventory(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Reserve inventory for an order."""
|
||||
@@ -62,7 +62,7 @@ def reserve_inventory(
|
||||
def release_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Release reserved inventory (cancel order)."""
|
||||
@@ -73,7 +73,7 @@ def release_reservation(
|
||||
def fulfill_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Fulfill reservation (complete order, remove from stock)."""
|
||||
@@ -84,7 +84,7 @@ def fulfill_reservation(
|
||||
def get_product_inventory(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get inventory summary for a product."""
|
||||
@@ -98,7 +98,7 @@ def get_vendor_inventory(
|
||||
location: Optional[str] = Query(None),
|
||||
low_stock: Optional[int] = Query(None, ge=0),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all inventory for vendor."""
|
||||
@@ -122,7 +122,7 @@ def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update inventory entry."""
|
||||
@@ -133,7 +133,7 @@ def update_inventory(
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
|
||||
8
app/api/v1/vendor/marketplace.py
vendored
8
app/api/v1/vendor/marketplace.py
vendored
@@ -10,7 +10,7 @@ from typing import List, Optional
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context # IMPORTANT
|
||||
from app.services.marketplace_import_job_service import marketplace_import_job_service
|
||||
@@ -33,7 +33,7 @@ async def import_products_from_marketplace(
|
||||
request: MarketplaceImportJobRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ADDED: Vendor from middleware
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||
@@ -79,7 +79,7 @@ async def import_products_from_marketplace(
|
||||
def get_marketplace_import_status(
|
||||
job_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get status of marketplace import job (Protected)."""
|
||||
@@ -99,7 +99,7 @@ def get_marketplace_import_jobs(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get marketplace import jobs for current vendor (Protected)."""
|
||||
|
||||
18
app/api/v1/vendor/media.py
vendored
18
app/api/v1/vendor/media.py
vendored
@@ -9,7 +9,7 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
@@ -26,7 +26,7 @@ def get_media_library(
|
||||
media_type: Optional[str] = Query(None, description="image, video, document"),
|
||||
search: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -53,7 +53,7 @@ async def upload_media(
|
||||
file: UploadFile = File(...),
|
||||
folder: Optional[str] = Query(None, description="products, general, etc."),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -79,7 +79,7 @@ async def upload_multiple_media(
|
||||
files: list[UploadFile] = File(...),
|
||||
folder: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -102,7 +102,7 @@ async def upload_multiple_media(
|
||||
def get_media_details(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -123,7 +123,7 @@ def update_media_metadata(
|
||||
media_id: int,
|
||||
metadata: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -144,7 +144,7 @@ def update_media_metadata(
|
||||
def delete_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -166,7 +166,7 @@ def delete_media(
|
||||
def get_media_usage(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -188,7 +188,7 @@ def get_media_usage(
|
||||
def optimize_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
|
||||
22
app/api/v1/vendor/notifications.py
vendored
22
app/api/v1/vendor/notifications.py
vendored
@@ -9,7 +9,7 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
@@ -25,7 +25,7 @@ def get_notifications(
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
unread_only: Optional[bool] = Query(False),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -48,7 +48,7 @@ def get_notifications(
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -68,7 +68,7 @@ def get_unread_count(
|
||||
def mark_as_read(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -86,7 +86,7 @@ def mark_as_read(
|
||||
@router.put("/mark-all-read")
|
||||
def mark_all_as_read(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -105,7 +105,7 @@ def mark_all_as_read(
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -123,7 +123,7 @@ def delete_notification(
|
||||
@router.get("/settings")
|
||||
def get_notification_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -146,7 +146,7 @@ def get_notification_settings(
|
||||
def update_notification_settings(
|
||||
settings: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -165,7 +165,7 @@ def update_notification_settings(
|
||||
@router.get("/templates")
|
||||
def get_notification_templates(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -187,7 +187,7 @@ def update_notification_template(
|
||||
template_id: int,
|
||||
template_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -208,7 +208,7 @@ def update_notification_template(
|
||||
def send_test_notification(
|
||||
notification_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
|
||||
22
app/api/v1/vendor/orders.py
vendored
22
app/api/v1/vendor/orders.py
vendored
@@ -6,10 +6,10 @@ Vendor order management endpoints.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, Request, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.order_service import order_service
|
||||
@@ -20,7 +20,7 @@ from models.schema.order import (
|
||||
OrderUpdate
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.vendor import Vendor, VendorUser
|
||||
|
||||
router = APIRouter(prefix="/orders")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,7 +33,7 @@ def get_vendor_orders(
|
||||
status: Optional[str] = Query(None, description="Filter by order status"),
|
||||
customer_id: Optional[int] = Query(None, description="Filter by customer"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -42,6 +42,8 @@ def get_vendor_orders(
|
||||
Supports filtering by:
|
||||
- status: Order status (pending, processing, shipped, delivered, cancelled)
|
||||
- customer_id: Filter orders from specific customer
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
orders, total = order_service.get_vendor_orders(
|
||||
db=db,
|
||||
@@ -64,10 +66,14 @@ def get_vendor_orders(
|
||||
def get_order_details(
|
||||
order_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed order information including items and addresses."""
|
||||
"""
|
||||
Get detailed order information including items and addresses.
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
order = order_service.get_order(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
@@ -82,7 +88,7 @@ def update_order_status(
|
||||
order_id: int,
|
||||
order_update: OrderUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -95,6 +101,8 @@ def update_order_status(
|
||||
- delivered: Order delivered
|
||||
- cancelled: Order cancelled
|
||||
- refunded: Order refunded
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
order = order_service.update_order_status(
|
||||
db=db,
|
||||
|
||||
18
app/api/v1/vendor/payments.py
vendored
18
app/api/v1/vendor/payments.py
vendored
@@ -8,7 +8,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
@router.get("/config")
|
||||
def get_payment_configuration(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -46,7 +46,7 @@ def get_payment_configuration(
|
||||
def update_payment_configuration(
|
||||
payment_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -67,7 +67,7 @@ def update_payment_configuration(
|
||||
def connect_stripe_account(
|
||||
stripe_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -87,7 +87,7 @@ def connect_stripe_account(
|
||||
@router.delete("/stripe/disconnect")
|
||||
def disconnect_stripe_account(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -106,7 +106,7 @@ def disconnect_stripe_account(
|
||||
@router.get("/methods")
|
||||
def get_payment_methods(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -125,7 +125,7 @@ def get_payment_methods(
|
||||
@router.get("/transactions")
|
||||
def get_payment_transactions(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -147,7 +147,7 @@ def get_payment_transactions(
|
||||
@router.get("/balance")
|
||||
def get_payment_balance(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -173,7 +173,7 @@ def refund_payment(
|
||||
payment_id: int,
|
||||
refund_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
|
||||
18
app/api/v1/vendor/products.py
vendored
18
app/api/v1/vendor/products.py
vendored
@@ -9,7 +9,7 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.product_service import product_service
|
||||
@@ -34,7 +34,7 @@ def get_vendor_products(
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -65,7 +65,7 @@ def get_vendor_products(
|
||||
def get_product_details(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed product information including inventory."""
|
||||
@@ -82,7 +82,7 @@ def get_product_details(
|
||||
def add_product_to_catalog(
|
||||
product_data: ProductCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -109,7 +109,7 @@ def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
@@ -132,7 +132,7 @@ def update_product(
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
@@ -154,7 +154,7 @@ def remove_product_from_catalog(
|
||||
def publish_from_marketplace(
|
||||
marketplace_product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
@@ -185,7 +185,7 @@ def publish_from_marketplace(
|
||||
def toggle_product_active(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product active status."""
|
||||
@@ -208,7 +208,7 @@ def toggle_product_active(
|
||||
def toggle_product_featured(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product featured status."""
|
||||
|
||||
6
app/api/v1/vendor/profile.py
vendored
6
app/api/v1/vendor/profile.py
vendored
@@ -7,7 +7,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
@router.get("", response_model=VendorResponse)
|
||||
def get_vendor_profile(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
@@ -33,7 +33,7 @@ def get_vendor_profile(
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
|
||||
6
app/api/v1/vendor/settings.py
vendored
6
app/api/v1/vendor/settings.py
vendored
@@ -7,7 +7,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
@router.get("")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
@@ -46,7 +46,7 @@ def get_vendor_settings(
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
|
||||
6
app/api/v1/vendor/team.py
vendored
6
app/api/v1/vendor/team.py
vendored
@@ -18,7 +18,7 @@ from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.core.permissions import VendorPermissions
|
||||
from app.api.deps import (
|
||||
get_current_vendor_from_cookie_or_header,
|
||||
get_current_vendor_api,
|
||||
require_vendor_owner,
|
||||
require_vendor_permission,
|
||||
get_user_permissions
|
||||
@@ -417,7 +417,7 @@ def list_roles(
|
||||
def get_my_permissions(
|
||||
request: Request,
|
||||
permissions: List[str] = Depends(get_user_permissions),
|
||||
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
current_user: User = Depends(get_current_vendor_api)
|
||||
):
|
||||
"""
|
||||
Get current user's permissions in this vendor.
|
||||
@@ -431,6 +431,8 @@ def get_my_permissions(
|
||||
- Complete list of permissions
|
||||
- Whether user is owner
|
||||
- Role name (if team member)
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
vendor = request.state.vendor
|
||||
|
||||
|
||||
@@ -74,8 +74,11 @@ class StatsService:
|
||||
).count()
|
||||
|
||||
# Staging statistics
|
||||
# TODO: This is fragile - MarketplaceProduct uses vendor_name (string) not vendor_id
|
||||
# Should add vendor_id foreign key to MarketplaceProduct for robust querying
|
||||
# For now, matching by vendor name which could fail if names don't match exactly
|
||||
staging_products = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.vendor_id == vendor_id
|
||||
MarketplaceProduct.vendor_name == vendor.name
|
||||
).count()
|
||||
|
||||
# Inventory statistics
|
||||
|
||||
11
app/templates/vendor/admin/customers.html
vendored
11
app/templates/vendor/admin/customers.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Customer management</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Customer management -->
|
||||
</body>
|
||||
</html>
|
||||
158
app/templates/vendor/admin/dashboard.html
vendored
158
app/templates/vendor/admin/dashboard.html
vendored
@@ -1,158 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vendor Dashboard - Multi-Tenant Ecommerce Platform</title>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="vendorDashboard()" x-init="init()" x-cloak>
|
||||
<!-- Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<h1>🏪 <span x-text="vendor?.name || 'Vendor Dashboard'"></span></h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-info">
|
||||
Welcome, <strong x-text="currentUser.username"></strong>
|
||||
<span class="badge badge-primary" x-text="vendorRole" style="margin-left: 8px;"></span>
|
||||
</span>
|
||||
<button class="btn-logout" @click="handleLogout">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="admin-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="admin-sidebar">
|
||||
<nav>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link active"
|
||||
@click.prevent="currentSection = 'dashboard'">
|
||||
📊 Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link"
|
||||
@click.prevent="currentSection = 'products'">
|
||||
📦 Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link"
|
||||
@click.prevent="currentSection = 'orders'">
|
||||
🛒 Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link"
|
||||
@click.prevent="currentSection = 'customers'">
|
||||
👥 Customers
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link"
|
||||
@click.prevent="currentSection = 'marketplace'">
|
||||
🌐 Marketplace
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="admin-content">
|
||||
<!-- Dashboard View -->
|
||||
<div x-show="currentSection === 'dashboard'">
|
||||
<!-- Vendor Info Card -->
|
||||
<div class="content-section" style="margin-bottom: 24px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<h2 style="margin: 0;" x-text="vendor?.name"></h2>
|
||||
<p style="margin: 4px 0 0 0; color: var(--text-secondary);">
|
||||
<strong x-text="vendor?.vendor_code"></strong> •
|
||||
<span x-text="vendor?.subdomain"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge"
|
||||
:class="vendor?.is_verified ? 'badge-success' : 'badge-warning'"
|
||||
x-text="vendor?.is_verified ? 'Verified' : 'Pending Verification'"></span>
|
||||
<span class="badge"
|
||||
:class="vendor?.is_active ? 'badge-success' : 'badge-danger'"
|
||||
x-text="vendor?.is_active ? 'Active' : 'Inactive'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<div class="stat-title">Products</div>
|
||||
<div class="stat-icon">📦</div>
|
||||
</div>
|
||||
<div class="stat-value" x-text="stats.products_count || 0"></div>
|
||||
<div class="stat-subtitle">in catalog</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<div class="stat-title">Orders</div>
|
||||
<div class="stat-icon">🛒</div>
|
||||
</div>
|
||||
<div class="stat-value" x-text="stats.orders_count || 0"></div>
|
||||
<div class="stat-subtitle">total orders</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<div class="stat-title">Customers</div>
|
||||
<div class="stat-icon">👥</div>
|
||||
</div>
|
||||
<div class="stat-value" x-text="stats.customers_count || 0"></div>
|
||||
<div class="stat-subtitle">registered</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<div class="stat-title">Revenue</div>
|
||||
<div class="stat-icon">💰</div>
|
||||
</div>
|
||||
<div class="stat-value">€<span x-text="stats.revenue || 0"></span></div>
|
||||
<div class="stat-subtitle">total revenue</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coming Soon Notice -->
|
||||
<div class="content-section" style="text-align: center; padding: 48px;">
|
||||
<h3>🚀 Getting Started</h3>
|
||||
<p class="text-muted" style="margin: 16px 0;">
|
||||
Welcome to your vendor dashboard! Start by importing products from the marketplace.
|
||||
</p>
|
||||
<button class="btn btn-primary" @click="currentSection = 'marketplace'">
|
||||
Go to Marketplace Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other sections -->
|
||||
<div x-show="currentSection !== 'dashboard'">
|
||||
<div class="content-section">
|
||||
<h2 x-text="currentSection.charAt(0).toUpperCase() + currentSection.slice(1)"></h2>
|
||||
<p class="text-muted">This section is coming soon in the next development slice.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
<script src="/static/vendor/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/inventory.html
vendored
11
app/templates/vendor/admin/inventory.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inventory management - catalog products</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Inventory management - catalog products -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Browse marketplace products - staging</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Browse marketplace products - staging -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Marketplace configuration</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Marketplace configuration -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Import jobs and history</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Import jobs and history -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Selected products - pre-publish</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Selected products - pre-publish -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/media.html
vendored
11
app/templates/vendor/admin/media.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Media library</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Media library -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/notifications.html
vendored
11
app/templates/vendor/admin/notifications.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification templates and logs</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Notification templates and logs -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/orders.html
vendored
11
app/templates/vendor/admin/orders.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Order management</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Order management -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/payments.html
vendored
11
app/templates/vendor/admin/payments.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment configuration</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Payment configuration -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/products.html
vendored
11
app/templates/vendor/admin/products.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Catalog management - Product table</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Catalog management - Product table -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/settings.html
vendored
11
app/templates/vendor/admin/settings.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vendor settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Vendor settings -->
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/vendor/admin/teams.html
vendored
11
app/templates/vendor/admin/teams.html
vendored
@@ -1,11 +0,0 @@
|
||||
<DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Team management</title>
|
||||
</head>
|
||||
<body>
|
||||
<-- Team management -->
|
||||
</body>
|
||||
</html>
|
||||
64
app/templates/vendor/partials/header.html
vendored
64
app/templates/vendor/partials/header.html
vendored
@@ -67,10 +67,10 @@
|
||||
</li>
|
||||
|
||||
<!-- Profile menu -->
|
||||
<li class="relative">
|
||||
<li class="relative" x-data="{ profileOpen: false }">
|
||||
<button class="align-middle rounded-full focus:shadow-outline-purple focus:outline-none"
|
||||
@click="toggleProfileMenu"
|
||||
@keydown.escape="closeProfileMenu"
|
||||
@click="profileOpen = !profileOpen"
|
||||
@keydown.escape="profileOpen = false"
|
||||
aria-label="Account"
|
||||
aria-haspopup="true">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
@@ -78,30 +78,40 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<template x-if="isProfileMenuOpen">
|
||||
<ul x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.away="closeProfileMenu"
|
||||
@keydown.escape="closeProfileMenu"
|
||||
class="absolute right-0 w-56 p-2 mt-2 space-y-2 text-gray-600 bg-white border border-gray-100 rounded-md shadow-md dark:border-gray-700 dark:text-gray-300 dark:bg-gray-700">
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
href="/vendor/{{ vendor_code }}/settings">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
@click.prevent="handleLogout"
|
||||
href="#">
|
||||
<span x-html="$icon('logout', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Log out</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<!-- Use x-show instead of x-if for reliability -->
|
||||
<ul x-show="profileOpen"
|
||||
x-cloak
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.away="profileOpen = false"
|
||||
@keydown.escape="profileOpen = false"
|
||||
class="absolute right-0 w-56 p-2 mt-2 space-y-2 text-gray-600 bg-white border border-gray-100 rounded-md shadow-md dark:border-gray-700 dark:text-gray-300 dark:bg-gray-700 z-50"
|
||||
style="display: none;"
|
||||
aria-label="submenu">
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
:href="`/vendor/${vendorCode}/profile`">
|
||||
<span x-html="$icon('user', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
:href="`/vendor/${vendorCode}/settings`">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<button
|
||||
@click="handleLogout()"
|
||||
class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200 text-left">
|
||||
<span x-html="$icon('logout', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Log out</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
11
app/templates/vendor/partials/sidebar.html
vendored
11
app/templates/vendor/partials/sidebar.html
vendored
@@ -129,6 +129,17 @@ Follows same pattern as admin sidebar
|
||||
<span class="ml-4">Team</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'profile'"
|
||||
class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"
|
||||
aria-hidden="true"></span>
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="currentPage === 'profile' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
:href="`/vendor/${vendorCode}/profile`">
|
||||
<span x-html="$icon('user', 'w-5 h-5')"></span>
|
||||
<span class="ml-4">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'settings'"
|
||||
class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"
|
||||
|
||||
@@ -117,6 +117,8 @@ nav:
|
||||
- Testing:
|
||||
- Testing Guide: testing/testing-guide.md
|
||||
- Test Maintenance: testing/test-maintenance.md
|
||||
- Test Structure: testing/test-structure.md
|
||||
- Vendor API Testing: testing/vendor-api-testing.md
|
||||
|
||||
# ============================================
|
||||
# DEPLOYMENT
|
||||
|
||||
48
static/vendor/js/dashboard.js
vendored
48
static/vendor/js/dashboard.js
vendored
@@ -3,8 +3,23 @@
|
||||
* Vendor dashboard page logic
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
const vendorDashLog = window.LogConfig.loggers.dashboard;
|
||||
|
||||
console.log('[VENDOR DASHBOARD] Loading...');
|
||||
console.log('[VENDOR DASHBOARD] data function exists?', typeof data);
|
||||
|
||||
function vendorDashboard() {
|
||||
console.log('[VENDOR DASHBOARD] vendorDashboard() called');
|
||||
console.log('[VENDOR DASHBOARD] data function exists inside?', typeof data);
|
||||
|
||||
return {
|
||||
// ✅ Inherit base layout state (includes vendorCode, dark mode, menu states)
|
||||
...data(),
|
||||
|
||||
// ✅ Set page identifier
|
||||
currentPage: 'dashboard',
|
||||
|
||||
loading: false,
|
||||
error: '',
|
||||
stats: {
|
||||
@@ -17,6 +32,18 @@ function vendorDashboard() {
|
||||
recentProducts: [],
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorDashboardInitialized) {
|
||||
return;
|
||||
}
|
||||
window._vendorDashboardInitialized = true;
|
||||
|
||||
// IMPORTANT: Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
await this.loadDashboardData();
|
||||
},
|
||||
|
||||
@@ -26,31 +53,40 @@ function vendorDashboard() {
|
||||
|
||||
try {
|
||||
// Load stats
|
||||
// NOTE: apiClient prepends /api/v1, and vendor context middleware handles vendor detection
|
||||
// So we just call /vendor/dashboard/stats → becomes /api/v1/vendor/dashboard/stats
|
||||
const statsResponse = await apiClient.get(
|
||||
`/api/v1/vendors/${this.vendorCode}/stats`
|
||||
`/vendor/dashboard/stats`
|
||||
);
|
||||
this.stats = statsResponse;
|
||||
|
||||
// Map API response to stats (similar to admin dashboard pattern)
|
||||
this.stats = {
|
||||
products_count: statsResponse.products?.total || 0,
|
||||
orders_count: statsResponse.orders?.total || 0,
|
||||
customers_count: statsResponse.customers?.total || 0,
|
||||
revenue: statsResponse.revenue?.total || 0
|
||||
};
|
||||
|
||||
// Load recent orders
|
||||
const ordersResponse = await apiClient.get(
|
||||
`/api/v1/vendors/${this.vendorCode}/orders?limit=5&sort=created_at:desc`
|
||||
`/vendor/orders?limit=5&sort=created_at:desc`
|
||||
);
|
||||
this.recentOrders = ordersResponse.items || [];
|
||||
|
||||
// Load recent products
|
||||
const productsResponse = await apiClient.get(
|
||||
`/api/v1/vendors/${this.vendorCode}/products?limit=5&sort=created_at:desc`
|
||||
`/vendor/products?limit=5&sort=created_at:desc`
|
||||
);
|
||||
this.recentProducts = productsResponse.items || [];
|
||||
|
||||
logInfo('Dashboard data loaded', {
|
||||
vendorDashLog.info('Dashboard data loaded', {
|
||||
stats: this.stats,
|
||||
orders: this.recentOrders.length,
|
||||
products: this.recentProducts.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logError('Failed to load dashboard data', error);
|
||||
vendorDashLog.error('Failed to load dashboard data', error);
|
||||
this.error = 'Failed to load dashboard data. Please try refreshing the page.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
|
||||
29
static/vendor/js/init-alpine.js
vendored
29
static/vendor/js/init-alpine.js
vendored
@@ -4,7 +4,13 @@
|
||||
* Provides common data and methods for all vendor pages
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
const vendorLog = window.LogConfig.log;
|
||||
|
||||
console.log('[VENDOR INIT-ALPINE] Loading...');
|
||||
|
||||
function data() {
|
||||
console.log('[VENDOR INIT-ALPINE] data() function called');
|
||||
return {
|
||||
dark: false,
|
||||
isSideMenuOpen: false,
|
||||
@@ -46,11 +52,12 @@ function data() {
|
||||
if (!this.vendorCode) return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/api/v1/vendors/${this.vendorCode}`);
|
||||
// apiClient prepends /api/v1, so /vendor/{code} → /api/v1/vendor/{code}
|
||||
const response = await apiClient.get(`/vendor/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
logDebug('Vendor info loaded', this.vendor);
|
||||
vendorLog.debug('Vendor info loaded', this.vendor);
|
||||
} catch (error) {
|
||||
logError('Failed to load vendor info', error);
|
||||
vendorLog.error('Failed to load vendor info', error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,13 +97,23 @@ function data() {
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
console.log('🚪 Logging out vendor user...');
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/v1/vendor/auth/logout');
|
||||
// Call logout API
|
||||
await apiClient.post('/vendor/auth/logout');
|
||||
console.log('✅ Logout API called successfully');
|
||||
} catch (error) {
|
||||
logError('Logout error', error);
|
||||
console.error('⚠️ Logout API error (continuing anyway):', error);
|
||||
} finally {
|
||||
localStorage.removeItem('accessToken');
|
||||
// Clear all tokens and data
|
||||
console.log('🧹 Clearing tokens...');
|
||||
localStorage.removeItem('vendor_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
localStorage.removeItem('vendorCode');
|
||||
localStorage.clear(); // Clear everything to be safe
|
||||
|
||||
console.log('🔄 Redirecting to login...');
|
||||
window.location.href = `/vendor/${this.vendorCode}/login`;
|
||||
}
|
||||
}
|
||||
|
||||
4
static/vendor/js/login.js
vendored
4
static/vendor/js/login.js
vendored
@@ -105,9 +105,11 @@ function vendorLogin() {
|
||||
vendorLoginLog.info('Login successful!');
|
||||
vendorLoginLog.debug('Storing authentication data...');
|
||||
|
||||
localStorage.setItem('accessToken', response.access_token);
|
||||
// Store token with correct key that apiClient expects
|
||||
localStorage.setItem('vendor_token', response.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
||||
localStorage.setItem('vendorCode', this.vendorCode);
|
||||
vendorLoginLog.debug('Token stored as vendor_token in localStorage');
|
||||
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
vendorLoginLog.info('Redirecting to vendor dashboard...');
|
||||
|
||||
31
tests/fixtures/auth_fixtures.py
vendored
31
tests/fixtures/auth_fixtures.py
vendored
@@ -114,3 +114,34 @@ def admin_headers(client, test_admin):
|
||||
assert response.status_code == 200, f"Admin login failed: {response.text}"
|
||||
token = response.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_vendor_user(db, auth_manager):
|
||||
"""Create a test vendor user with unique username"""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
hashed_password = auth_manager.hash_password("vendorpass123")
|
||||
user = User(
|
||||
email=f"vendor_{unique_id}@example.com",
|
||||
username=f"vendoruser_{unique_id}",
|
||||
hashed_password=hashed_password,
|
||||
role="vendor",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
db.expunge(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vendor_user_headers(client, test_vendor_user):
|
||||
"""Get authentication headers for vendor user (uses get_current_vendor_api)"""
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": test_vendor_user.username, "password": "vendorpass123"},
|
||||
)
|
||||
assert response.status_code == 200, f"Vendor login failed: {response.text}"
|
||||
token = response.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
33
tests/fixtures/vendor_fixtures.py
vendored
33
tests/fixtures/vendor_fixtures.py
vendored
@@ -27,6 +27,39 @@ def test_vendor(db, test_user):
|
||||
return vendor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_vendor_with_vendor_user(db, test_vendor_user):
|
||||
"""Create a vendor owned by a vendor user (for testing vendor API endpoints)"""
|
||||
from models.database.vendor import VendorUser
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8].upper()
|
||||
vendor = Vendor(
|
||||
vendor_code=f"VENDORAPI_{unique_id}",
|
||||
subdomain=f"vendorapi{unique_id.lower()}",
|
||||
name=f"Vendor API Test {unique_id}",
|
||||
owner_user_id=test_vendor_user.id,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(vendor)
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
# Create VendorUser association
|
||||
vendor_user = VendorUser(
|
||||
vendor_id=vendor.id,
|
||||
user_id=test_vendor_user.id,
|
||||
is_owner=True,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(vendor_user)
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
db.expunge(vendor)
|
||||
return vendor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unique_vendor(db, test_user):
|
||||
"""Create a unique vendor for tests that need isolated vendor data"""
|
||||
|
||||
Reference in New Issue
Block a user