Fixing vendor dashboard area

This commit is contained in:
2025-11-21 23:15:25 +01:00
parent 5aff76a27e
commit 86f1e16ef2
38 changed files with 312 additions and 433 deletions

View File

@@ -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"]

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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),
):
"""

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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)."""

View File

@@ -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),
):
"""

View File

@@ -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),
):
"""

View File

@@ -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,

View File

@@ -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),
):
"""

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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;

View File

@@ -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`;
}
}

View File

@@ -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...');

View File

@@ -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}"}

View File

@@ -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"""