vendor features for admin and vendor admin area
This commit is contained in:
@@ -12,55 +12,89 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from models.schema.vendor import VendorListResponse, VendorResponse, VendorCreate
|
||||
from models.schema.vendor import (
|
||||
VendorListResponse,
|
||||
VendorResponse,
|
||||
VendorDetailResponse,
|
||||
VendorCreate,
|
||||
VendorCreateResponse,
|
||||
VendorUpdate,
|
||||
VendorTransferOwnership,
|
||||
VendorTransferOwnershipResponse,
|
||||
)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/vendors")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("", response_model=VendorResponse)
|
||||
@router.post("", response_model=VendorCreateResponse)
|
||||
def create_vendor_with_owner(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Create a new vendor with owner user account (Admin only).
|
||||
|
||||
This endpoint:
|
||||
1. Creates a new vendor
|
||||
2. Creates an owner user account for the vendor
|
||||
3. Sets up default roles (Owner, Manager, Editor, Viewer)
|
||||
4. Sends welcome email to vendor owner (if email service configured)
|
||||
1. Creates a new vendor record
|
||||
2. Creates an owner user account with owner_email
|
||||
3. Sets contact_email (defaults to owner_email if not provided)
|
||||
4. Sets up default roles (Owner, Manager, Editor, Viewer)
|
||||
5. Returns credentials (temporary password shown ONCE)
|
||||
|
||||
Returns the created vendor with owner information.
|
||||
**Email Fields:**
|
||||
- `owner_email`: Used for owner's login/authentication (stored in users.email)
|
||||
- `contact_email`: Public business contact (stored in vendors.contact_email)
|
||||
|
||||
Returns vendor details with owner credentials.
|
||||
"""
|
||||
vendor, owner_user, temp_password = admin_service.create_vendor_with_owner(
|
||||
db=db,
|
||||
vendor_data=vendor_data
|
||||
)
|
||||
|
||||
return {
|
||||
**VendorResponse.model_validate(vendor).model_dump(),
|
||||
"owner_email": owner_user.email,
|
||||
"owner_username": owner_user.username,
|
||||
"temporary_password": temp_password, # Only shown once!
|
||||
"login_url": f"{vendor.subdomain}.platform.com/vendor/login" if vendor.subdomain else None
|
||||
}
|
||||
return VendorCreateResponse(
|
||||
# Vendor fields
|
||||
id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
subdomain=vendor.subdomain,
|
||||
name=vendor.name,
|
||||
description=vendor.description,
|
||||
owner_user_id=vendor.owner_user_id,
|
||||
contact_email=vendor.contact_email,
|
||||
contact_phone=vendor.contact_phone,
|
||||
website=vendor.website,
|
||||
business_address=vendor.business_address,
|
||||
tax_number=vendor.tax_number,
|
||||
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
|
||||
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
|
||||
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
|
||||
theme_config=vendor.theme_config or {},
|
||||
is_active=vendor.is_active,
|
||||
is_verified=vendor.is_verified,
|
||||
created_at=vendor.created_at,
|
||||
updated_at=vendor.updated_at,
|
||||
# Owner credentials
|
||||
owner_email=owner_user.email,
|
||||
owner_username=owner_user.username,
|
||||
temporary_password=temp_password,
|
||||
login_url=f"http://localhost:8000/vendor/{vendor.subdomain}/login"
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=VendorListResponse)
|
||||
def get_all_vendors_admin(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None, description="Search by name or vendor code"),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_verified: Optional[bool] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None, description="Search by name or vendor code"),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_verified: Optional[bool] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all vendors with admin view (Admin only)."""
|
||||
"""Get all vendors with filtering (Admin only)."""
|
||||
vendors, total = admin_service.get_all_vendors(
|
||||
db=db,
|
||||
skip=skip,
|
||||
@@ -72,15 +106,144 @@ def get_all_vendors_admin(
|
||||
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}", response_model=VendorResponse)
|
||||
@router.get("/{vendor_id}", response_model=VendorDetailResponse)
|
||||
def get_vendor_details(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get detailed vendor information (Admin only)."""
|
||||
"""
|
||||
Get detailed vendor information including owner details (Admin only).
|
||||
|
||||
Returns both:
|
||||
- `contact_email` (business contact)
|
||||
- `owner_email` (owner's authentication email)
|
||||
"""
|
||||
vendor = admin_service.get_vendor_by_id(db, vendor_id)
|
||||
return VendorResponse.model_validate(vendor)
|
||||
|
||||
return VendorDetailResponse(
|
||||
# Vendor fields
|
||||
id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
subdomain=vendor.subdomain,
|
||||
name=vendor.name,
|
||||
description=vendor.description,
|
||||
owner_user_id=vendor.owner_user_id,
|
||||
contact_email=vendor.contact_email,
|
||||
contact_phone=vendor.contact_phone,
|
||||
website=vendor.website,
|
||||
business_address=vendor.business_address,
|
||||
tax_number=vendor.tax_number,
|
||||
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
|
||||
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
|
||||
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
|
||||
theme_config=vendor.theme_config or {},
|
||||
is_active=vendor.is_active,
|
||||
is_verified=vendor.is_verified,
|
||||
created_at=vendor.created_at,
|
||||
updated_at=vendor.updated_at,
|
||||
# Owner details
|
||||
owner_email=vendor.owner.email,
|
||||
owner_username=vendor.owner.username,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{vendor_id}", response_model=VendorDetailResponse)
|
||||
def update_vendor(
|
||||
vendor_id: int,
|
||||
vendor_update: VendorUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Update vendor information (Admin only).
|
||||
|
||||
**Can update:**
|
||||
- Basic info: name, description, subdomain
|
||||
- Business contact: contact_email, contact_phone, website
|
||||
- Business details: business_address, tax_number
|
||||
- Marketplace URLs
|
||||
- Status: is_active, is_verified
|
||||
|
||||
**Cannot update:**
|
||||
- `owner_email` (use POST /vendors/{id}/transfer-ownership)
|
||||
- `vendor_code` (immutable)
|
||||
- `owner_user_id` (use POST /vendors/{id}/transfer-ownership)
|
||||
"""
|
||||
vendor = admin_service.update_vendor(db, vendor_id, vendor_update)
|
||||
|
||||
return VendorDetailResponse(
|
||||
id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
subdomain=vendor.subdomain,
|
||||
name=vendor.name,
|
||||
description=vendor.description,
|
||||
owner_user_id=vendor.owner_user_id,
|
||||
contact_email=vendor.contact_email,
|
||||
contact_phone=vendor.contact_phone,
|
||||
website=vendor.website,
|
||||
business_address=vendor.business_address,
|
||||
tax_number=vendor.tax_number,
|
||||
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
|
||||
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
|
||||
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
|
||||
theme_config=vendor.theme_config or {},
|
||||
is_active=vendor.is_active,
|
||||
is_verified=vendor.is_verified,
|
||||
created_at=vendor.created_at,
|
||||
updated_at=vendor.updated_at,
|
||||
owner_email=vendor.owner.email,
|
||||
owner_username=vendor.owner.username,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/transfer-ownership", response_model=VendorTransferOwnershipResponse)
|
||||
def transfer_vendor_ownership(
|
||||
vendor_id: int,
|
||||
transfer_data: VendorTransferOwnership,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Transfer vendor ownership to another user (Admin only).
|
||||
|
||||
**This is a critical operation that:**
|
||||
- Changes the owner_user_id
|
||||
- Assigns new owner to "Owner" role
|
||||
- Demotes old owner to "Manager" role (or removes them)
|
||||
- Creates audit trail
|
||||
|
||||
⚠️ **This action is logged and should be used carefully.**
|
||||
|
||||
**Requires:**
|
||||
- `new_owner_user_id`: ID of user who will become owner
|
||||
- `confirm_transfer`: Must be true
|
||||
- `transfer_reason`: Optional reason for audit trail
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
vendor, old_owner, new_owner = admin_service.transfer_vendor_ownership(
|
||||
db, vendor_id, transfer_data
|
||||
)
|
||||
|
||||
return VendorTransferOwnershipResponse(
|
||||
message="Ownership transferred successfully",
|
||||
vendor_id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
vendor_name=vendor.name,
|
||||
old_owner={
|
||||
"id": old_owner.id,
|
||||
"username": old_owner.username,
|
||||
"email": old_owner.email,
|
||||
},
|
||||
new_owner={
|
||||
"id": new_owner.id,
|
||||
"username": new_owner.username,
|
||||
"email": new_owner.email,
|
||||
},
|
||||
transferred_at=datetime.now(timezone.utc),
|
||||
transfer_reason=transfer_data.transfer_reason,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/verify")
|
||||
@@ -115,14 +278,14 @@ def delete_vendor(
|
||||
"""
|
||||
Delete vendor and all associated data (Admin only).
|
||||
|
||||
WARNING: This is destructive and will delete:
|
||||
⚠️ **WARNING: This is destructive and will delete:**
|
||||
- Vendor account
|
||||
- All products
|
||||
- All orders
|
||||
- All customers
|
||||
- All team members
|
||||
|
||||
Requires confirmation parameter.
|
||||
Requires confirmation parameter: `confirm=true`
|
||||
"""
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
|
||||
128
app/api/v1/public/vendors/vendors.py
vendored
Normal file
128
app/api/v1/public/vendors/vendors.py
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
# app/api/v1/public/vendors/vendors.py
|
||||
"""
|
||||
Public vendor information endpoints.
|
||||
|
||||
Provides public-facing vendor lookup and information retrieval.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/by-code/{vendor_code}")
|
||||
def get_vendor_by_code(
|
||||
vendor_code: str = Path(..., description="Vendor code (e.g., TECHSTORE233)"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get public vendor information by vendor code.
|
||||
|
||||
This endpoint is used by:
|
||||
- Frontend vendor login page to validate vendor existence
|
||||
- Customer shop to display vendor information
|
||||
|
||||
Returns basic vendor information (no sensitive data).
|
||||
"""
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.vendor_code == vendor_code.upper(),
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
logger.warning(f"Vendor lookup failed for code: {vendor_code}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Vendor '{vendor_code}' not found or inactive"
|
||||
)
|
||||
|
||||
logger.info(f"Vendor lookup successful: {vendor.vendor_code}")
|
||||
|
||||
# Return public vendor information (no sensitive data)
|
||||
return {
|
||||
"id": vendor.id,
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"description": vendor.description,
|
||||
"website": vendor.website,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified
|
||||
}
|
||||
|
||||
|
||||
@router.get("/by-subdomain/{subdomain}")
|
||||
def get_vendor_by_subdomain(
|
||||
subdomain: str = Path(..., description="Vendor subdomain (e.g., techstore233)"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get public vendor information by subdomain.
|
||||
|
||||
Used for subdomain-based vendor detection in production environments.
|
||||
Example: techstore233.platform.com -> subdomain is "techstore233"
|
||||
"""
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.subdomain == subdomain.lower(),
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
logger.warning(f"Vendor lookup failed for subdomain: {subdomain}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Vendor with subdomain '{subdomain}' not found or inactive"
|
||||
)
|
||||
|
||||
logger.info(f"Vendor lookup by subdomain successful: {vendor.vendor_code}")
|
||||
|
||||
return {
|
||||
"id": vendor.id,
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"description": vendor.description,
|
||||
"website": vendor.website,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/info")
|
||||
def get_vendor_info(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get public vendor information by ID.
|
||||
|
||||
Used when vendor_id is already known (e.g., from URL parameters).
|
||||
"""
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
logger.warning(f"Vendor lookup failed for ID: {vendor_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Vendor with ID {vendor_id} not found or inactive"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": vendor.id,
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"description": vendor.description,
|
||||
"website": vendor.website,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified
|
||||
}
|
||||
29
app/api/v1/vendor/analytics.py
vendored
Normal file
29
app/api/v1/vendor/analytics.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# app/api/v1/vendor/analytics.py
|
||||
"""
|
||||
Vendor analytics and reporting endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/analytics")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data for specified time period."""
|
||||
return stats_service.get_vendor_analytics(db, vendor.id, period)
|
||||
114
app/api/v1/vendor/auth.py
vendored
114
app/api/v1/vendor/auth.py
vendored
@@ -9,21 +9,32 @@ This module provides:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.auth_service import auth_service
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from middleware.vendor_context import get_current_vendor
|
||||
from models.schema.auth import LoginResponse, UserLogin
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.auth import UserLogin
|
||||
from models.database.vendor import Vendor, VendorUser, Role
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
# Response model for vendor login
|
||||
class VendorLoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
expires_in: int
|
||||
user: dict
|
||||
vendor: dict
|
||||
vendor_role: str
|
||||
|
||||
|
||||
@router.post("/login", response_model=VendorLoginResponse)
|
||||
def vendor_login(
|
||||
user_credentials: UserLogin,
|
||||
request: Request,
|
||||
@@ -35,6 +46,18 @@ def vendor_login(
|
||||
Authenticates users who are part of a vendor team.
|
||||
Validates against vendor context if available.
|
||||
"""
|
||||
# Try to get vendor from middleware first
|
||||
vendor = get_current_vendor(request)
|
||||
|
||||
# If no vendor from middleware, try to get from request body
|
||||
if not vendor and hasattr(user_credentials, 'vendor_code'):
|
||||
vendor_code = getattr(user_credentials, 'vendor_code', None)
|
||||
if vendor_code:
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.vendor_code == vendor_code.upper(),
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
# Authenticate user
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
user = login_result["user"]
|
||||
@@ -44,32 +67,79 @@ def vendor_login(
|
||||
logger.warning(f"Admin user attempted vendor login: {user.username}")
|
||||
raise InvalidCredentialsException("Please use admin portal to login")
|
||||
|
||||
# Optional: Validate user belongs to current vendor context
|
||||
vendor = get_current_vendor(request)
|
||||
# Determine vendor and role
|
||||
vendor_role = "Member"
|
||||
|
||||
if vendor:
|
||||
# Check if user is vendor owner or team member
|
||||
is_owner = any(v.id == vendor.id for v in user.owned_vendors)
|
||||
is_team_member = any(
|
||||
vm.vendor_id == vendor.id and vm.is_active
|
||||
for vm in user.vendor_memberships
|
||||
)
|
||||
# Check if user is vendor owner
|
||||
is_owner = vendor.owner_user_id == user.id
|
||||
|
||||
if not (is_owner or is_team_member):
|
||||
logger.warning(
|
||||
f"User {user.username} attempted login to vendor {vendor.vendor_code} "
|
||||
f"but is not authorized"
|
||||
if is_owner:
|
||||
vendor_role = "Owner"
|
||||
else:
|
||||
# Check if user is team member
|
||||
vendor_user = db.query(VendorUser).join(Role).filter(
|
||||
VendorUser.user_id == user.id,
|
||||
VendorUser.vendor_id == vendor.id,
|
||||
VendorUser.is_active == True
|
||||
).first()
|
||||
|
||||
if vendor_user:
|
||||
vendor_role = vendor_user.role.name
|
||||
else:
|
||||
logger.warning(
|
||||
f"User {user.username} attempted login to vendor {vendor.vendor_code} "
|
||||
f"but is not authorized"
|
||||
)
|
||||
raise InvalidCredentialsException(
|
||||
"You do not have access to this vendor"
|
||||
)
|
||||
else:
|
||||
# No vendor context - find which vendor this user belongs to
|
||||
# Check owned vendors first
|
||||
if user.owned_vendors:
|
||||
vendor = user.owned_vendors[0]
|
||||
vendor_role = "Owner"
|
||||
# Check vendor memberships
|
||||
elif user.vendor_memberships:
|
||||
active_membership = next(
|
||||
(vm for vm in user.vendor_memberships if vm.is_active),
|
||||
None
|
||||
)
|
||||
if active_membership:
|
||||
vendor = active_membership.vendor
|
||||
vendor_role = active_membership.role.name
|
||||
|
||||
if not vendor:
|
||||
raise InvalidCredentialsException(
|
||||
"You do not have access to this vendor"
|
||||
"User is not associated with any vendor"
|
||||
)
|
||||
|
||||
logger.info(f"Vendor team login successful: {user.username}")
|
||||
logger.info(
|
||||
f"Vendor team login successful: {user.username} "
|
||||
f"for vendor {vendor.vendor_code} as {vendor_role}"
|
||||
)
|
||||
|
||||
return LoginResponse(
|
||||
return VendorLoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
user=login_result["user"],
|
||||
user={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active
|
||||
},
|
||||
vendor={
|
||||
"id": vendor.id,
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified
|
||||
},
|
||||
vendor_role=vendor_role
|
||||
)
|
||||
|
||||
|
||||
@@ -80,4 +150,4 @@ def vendor_logout():
|
||||
|
||||
Client should remove token from storage.
|
||||
"""
|
||||
return {"message": "Logged out successfully"}
|
||||
return {"message": "Logged out successfully"}
|
||||
157
app/api/v1/vendor/customers.py
vendored
157
app/api/v1/vendor/customers.py
vendored
@@ -1 +1,156 @@
|
||||
# Vendor customer management
|
||||
# Vendor customer management
|
||||
# app/api/v1/vendor/customers.py
|
||||
"""
|
||||
Vendor customer management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
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.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/customers")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_customers(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all customers for this vendor.
|
||||
|
||||
TODO: Implement in Slice 4
|
||||
- Query customers filtered by vendor_id
|
||||
- Support search by name/email
|
||||
- Support filtering by active status
|
||||
- Return paginated results
|
||||
"""
|
||||
return {
|
||||
"customers": [],
|
||||
"total": 0,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"message": "Customer management coming in Slice 4"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}")
|
||||
def get_customer_details(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed customer information.
|
||||
|
||||
TODO: Implement in Slice 4
|
||||
- Get customer by ID
|
||||
- Verify customer belongs to vendor
|
||||
- Include order history
|
||||
- Include total spent, etc.
|
||||
"""
|
||||
return {
|
||||
"message": "Customer details coming in Slice 4"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/orders")
|
||||
def get_customer_orders(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get order history for a specific customer.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get all orders for customer
|
||||
- Filter by vendor_id
|
||||
- Return order details
|
||||
"""
|
||||
return {
|
||||
"orders": [],
|
||||
"message": "Customer orders coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{customer_id}")
|
||||
def update_customer(
|
||||
customer_id: int,
|
||||
customer_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update customer information.
|
||||
|
||||
TODO: Implement in Slice 4
|
||||
- Update customer details
|
||||
- Verify customer belongs to vendor
|
||||
- Update customer preferences
|
||||
"""
|
||||
return {
|
||||
"message": "Customer update coming in Slice 4"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{customer_id}/status")
|
||||
def toggle_customer_status(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Activate/deactivate customer account.
|
||||
|
||||
TODO: Implement in Slice 4
|
||||
- Toggle customer is_active status
|
||||
- Verify customer belongs to vendor
|
||||
- Log the change
|
||||
"""
|
||||
return {
|
||||
"message": "Customer status toggle coming in Slice 4"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/stats")
|
||||
def get_customer_statistics(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get customer statistics and metrics.
|
||||
|
||||
TODO: Implement in Slice 4
|
||||
- Total orders
|
||||
- Total spent
|
||||
- Average order value
|
||||
- Last order date
|
||||
"""
|
||||
return {
|
||||
"total_orders": 0,
|
||||
"total_spent": 0.0,
|
||||
"average_order_value": 0.0,
|
||||
"last_order_date": None,
|
||||
"message": "Customer statistics coming in Slice 4"
|
||||
}
|
||||
|
||||
|
||||
207
app/api/v1/vendor/media.py
vendored
207
app/api/v1/vendor/media.py
vendored
@@ -1 +1,206 @@
|
||||
# File and media management
|
||||
# File and media management
|
||||
# app/api/v1/vendor/media.py
|
||||
"""
|
||||
Vendor media and file management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
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.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_media_library(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor media library.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Get all media files for vendor
|
||||
- Filter by type (image, video, document)
|
||||
- Search by filename
|
||||
- Support pagination
|
||||
- Return file URLs, sizes, metadata
|
||||
"""
|
||||
return {
|
||||
"media": [],
|
||||
"total": 0,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"message": "Media library coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload media file.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Accept file upload
|
||||
- Validate file type and size
|
||||
- Store file (local or cloud storage)
|
||||
- Generate thumbnails for images
|
||||
- Save metadata to database
|
||||
- Return file URL
|
||||
"""
|
||||
return {
|
||||
"file_url": None,
|
||||
"thumbnail_url": None,
|
||||
"message": "Media upload coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/upload/multiple")
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload multiple media files at once.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Accept multiple files
|
||||
- Process each file
|
||||
- Return list of uploaded file URLs
|
||||
- Handle errors gracefully
|
||||
"""
|
||||
return {
|
||||
"uploaded_files": [],
|
||||
"failed_files": [],
|
||||
"message": "Multiple upload coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{media_id}")
|
||||
def get_media_details(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get media file details.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Get file metadata
|
||||
- Return file URL
|
||||
- Return usage information (which products use this file)
|
||||
"""
|
||||
return {
|
||||
"message": "Media details coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{media_id}")
|
||||
def update_media_metadata(
|
||||
media_id: int,
|
||||
metadata: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update media file metadata.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Update filename
|
||||
- Update alt text
|
||||
- Update tags/categories
|
||||
- Update description
|
||||
"""
|
||||
return {
|
||||
"message": "Media update coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{media_id}")
|
||||
def delete_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete media file.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Verify file belongs to vendor
|
||||
- Check if file is in use by products
|
||||
- Delete file from storage
|
||||
- Delete database record
|
||||
- Return success/error
|
||||
"""
|
||||
return {
|
||||
"message": "Media deletion coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{media_id}/usage")
|
||||
def get_media_usage(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get where this media file is being used.
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Check products using this media
|
||||
- Check other entities using this media
|
||||
- Return list of usage
|
||||
"""
|
||||
return {
|
||||
"products": [],
|
||||
"other_usage": [],
|
||||
"message": "Media usage tracking coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/optimize/{media_id}")
|
||||
def optimize_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Optimize media file (compress, resize, etc.).
|
||||
|
||||
TODO: Implement in Slice 3
|
||||
- Optimize image (compress, resize)
|
||||
- Generate multiple sizes
|
||||
- Keep original
|
||||
- Update database with new versions
|
||||
"""
|
||||
return {
|
||||
"message": "Media optimization coming in Slice 3"
|
||||
}
|
||||
|
||||
|
||||
223
app/api/v1/vendor/notifications.py
vendored
223
app/api/v1/vendor/notifications.py
vendored
@@ -1 +1,224 @@
|
||||
# Notification management
|
||||
# app/api/v1/vendor/notifications.py
|
||||
"""
|
||||
Vendor notification management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
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.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/notifications")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_notifications(
|
||||
skip: int = Query(0, ge=0),
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor notifications.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get all notifications for vendor
|
||||
- Filter by read/unread status
|
||||
- Support pagination
|
||||
- Return notification details
|
||||
"""
|
||||
return {
|
||||
"notifications": [],
|
||||
"total": 0,
|
||||
"unread_count": 0,
|
||||
"message": "Notifications coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get count of unread notifications.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Count unread notifications for vendor
|
||||
- Used for notification badge
|
||||
"""
|
||||
return {
|
||||
"unread_count": 0,
|
||||
"message": "Unread count coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
def mark_as_read(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Mark notification as read.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Mark single notification as read
|
||||
- Update read timestamp
|
||||
"""
|
||||
return {
|
||||
"message": "Mark as read coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/mark-all-read")
|
||||
def mark_all_as_read(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Mark all notifications as read.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Mark all vendor notifications as read
|
||||
- Update timestamps
|
||||
"""
|
||||
return {
|
||||
"message": "Mark all as read coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete notification.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Delete single notification
|
||||
- Verify notification belongs to vendor
|
||||
"""
|
||||
return {
|
||||
"message": "Notification deletion coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
def get_notification_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get notification preferences.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get email notification settings
|
||||
- Get in-app notification settings
|
||||
- Get notification types enabled/disabled
|
||||
"""
|
||||
return {
|
||||
"email_notifications": True,
|
||||
"in_app_notifications": True,
|
||||
"notification_types": {},
|
||||
"message": "Notification settings coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings")
|
||||
def update_notification_settings(
|
||||
settings: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update notification preferences.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Update email notification settings
|
||||
- Update in-app notification settings
|
||||
- Enable/disable specific notification types
|
||||
"""
|
||||
return {
|
||||
"message": "Notification settings update coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
def get_notification_templates(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get notification email templates.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get all notification templates
|
||||
- Include: order confirmation, shipping notification, etc.
|
||||
- Return template details
|
||||
"""
|
||||
return {
|
||||
"templates": [],
|
||||
"message": "Notification templates coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}")
|
||||
def update_notification_template(
|
||||
template_id: int,
|
||||
template_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update notification email template.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Update template subject
|
||||
- Update template body (HTML/text)
|
||||
- Validate template variables
|
||||
- Preview template
|
||||
"""
|
||||
return {
|
||||
"message": "Template update coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
def send_test_notification(
|
||||
notification_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Send test notification.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Send test email notification
|
||||
- Use specified template
|
||||
- Send to current user's email
|
||||
"""
|
||||
return {
|
||||
"message": "Test notification coming in Slice 5"
|
||||
}
|
||||
|
||||
189
app/api/v1/vendor/payments.py
vendored
189
app/api/v1/vendor/payments.py
vendored
@@ -1 +1,190 @@
|
||||
# Payment configuration and processing
|
||||
# app/api/v1/vendor/payments.py
|
||||
"""
|
||||
Vendor payment configuration and processing endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/payments")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def get_payment_configuration(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor payment configuration.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get payment gateway settings (Stripe, PayPal, etc.)
|
||||
- Get accepted payment methods
|
||||
- Get currency settings
|
||||
- Return masked/secure information only
|
||||
"""
|
||||
return {
|
||||
"payment_gateway": None,
|
||||
"accepted_methods": [],
|
||||
"currency": "EUR",
|
||||
"stripe_connected": False,
|
||||
"message": "Payment configuration coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/config")
|
||||
def update_payment_configuration(
|
||||
payment_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update vendor payment configuration.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Update payment gateway settings
|
||||
- Connect/disconnect Stripe account
|
||||
- Update accepted payment methods
|
||||
- Validate configuration before saving
|
||||
"""
|
||||
return {
|
||||
"message": "Payment configuration update coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/stripe/connect")
|
||||
def connect_stripe_account(
|
||||
stripe_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Connect Stripe account for payment processing.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Handle Stripe OAuth flow
|
||||
- Store Stripe account ID securely
|
||||
- Verify Stripe account is active
|
||||
- Enable payment processing
|
||||
"""
|
||||
return {
|
||||
"message": "Stripe connection coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/stripe/disconnect")
|
||||
def disconnect_stripe_account(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Disconnect Stripe account.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Remove Stripe account connection
|
||||
- Disable payment processing
|
||||
- Warn about pending payments
|
||||
"""
|
||||
return {
|
||||
"message": "Stripe disconnection coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/methods")
|
||||
def get_payment_methods(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get accepted payment methods for vendor.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Return list of enabled payment methods
|
||||
- Include: credit card, PayPal, bank transfer, etc.
|
||||
"""
|
||||
return {
|
||||
"methods": [],
|
||||
"message": "Payment methods coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
def get_payment_transactions(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get payment transaction history.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get all payment transactions for vendor
|
||||
- Filter by date range, status, etc.
|
||||
- Include payment details
|
||||
- Support pagination
|
||||
"""
|
||||
return {
|
||||
"transactions": [],
|
||||
"total": 0,
|
||||
"message": "Payment transactions coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/balance")
|
||||
def get_payment_balance(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor payment balance and payout information.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Get available balance
|
||||
- Get pending balance
|
||||
- Get next payout date
|
||||
- Get payout history
|
||||
"""
|
||||
return {
|
||||
"available_balance": 0.0,
|
||||
"pending_balance": 0.0,
|
||||
"currency": "EUR",
|
||||
"next_payout_date": None,
|
||||
"message": "Payment balance coming in Slice 5"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refund/{payment_id}")
|
||||
def refund_payment(
|
||||
payment_id: int,
|
||||
refund_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Process payment refund.
|
||||
|
||||
TODO: Implement in Slice 5
|
||||
- Verify payment belongs to vendor
|
||||
- Process refund through payment gateway
|
||||
- Update order status
|
||||
- Send refund notification to customer
|
||||
"""
|
||||
return {
|
||||
"message": "Payment refund coming in Slice 5"
|
||||
}
|
||||
|
||||
44
app/api/v1/vendor/profile.py
vendored
Normal file
44
app/api/v1/vendor/profile.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# app/api/v1/vendor/profile.py
|
||||
"""
|
||||
Vendor profile management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.schema.vendor import VendorUpdate, VendorResponse
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/profile")
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
return vendor
|
||||
|
||||
|
||||
@router.put("", response_model=VendorResponse)
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
# Verify user has permission to update vendor
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
return vendor_service.update_vendor(db, vendor.id, vendor_update)
|
||||
96
app/api/v1/vendor/settings.py
vendored
96
app/api/v1/vendor/settings.py
vendored
@@ -1 +1,95 @@
|
||||
# Vendor settings and configuration
|
||||
# app/api/v1/vendor/settings.py
|
||||
"""
|
||||
Vendor settings and configuration endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
return {
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"contact_email": vendor.contact_email,
|
||||
"contact_phone": vendor.contact_phone,
|
||||
"website": vendor.website,
|
||||
"business_address": vendor.business_address,
|
||||
"tax_number": vendor.tax_number,
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
"theme_config": vendor.theme_config,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/marketplace")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Verify permissions
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# Update Letzshop URLs
|
||||
if "letzshop_csv_url_fr" in marketplace_config:
|
||||
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
||||
if "letzshop_csv_url_en" in marketplace_config:
|
||||
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
||||
if "letzshop_csv_url_de" in marketplace_config:
|
||||
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
||||
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Marketplace settings updated successfully",
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/theme")
|
||||
def update_theme_settings(
|
||||
theme_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor theme configuration."""
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
vendor.theme_config = theme_config
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Theme settings updated successfully",
|
||||
"theme_config": vendor.theme_config,
|
||||
}
|
||||
|
||||
74
app/api/v1/vendor/teams.py
vendored
74
app/api/v1/vendor/teams.py
vendored
@@ -1 +1,73 @@
|
||||
# Team member management
|
||||
# app/api/v1/vendor/teams.py
|
||||
"""
|
||||
Vendor team member management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.team_service import team_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/teams")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/members")
|
||||
def get_team_members(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all team members for vendor."""
|
||||
return team_service.get_team_members(db, vendor.id, current_user)
|
||||
|
||||
|
||||
@router.post("/invite")
|
||||
def invite_team_member(
|
||||
invitation_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Invite a new team member."""
|
||||
return team_service.invite_team_member(db, vendor.id, invitation_data, current_user)
|
||||
|
||||
|
||||
@router.put("/members/{user_id}")
|
||||
def update_team_member(
|
||||
user_id: int,
|
||||
update_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update team member role or status."""
|
||||
return team_service.update_team_member(db, vendor.id, user_id, update_data, current_user)
|
||||
|
||||
|
||||
@router.delete("/members/{user_id}")
|
||||
def remove_team_member(
|
||||
user_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove team member from vendor."""
|
||||
team_service.remove_team_member(db, vendor.id, user_id, current_user)
|
||||
return {"message": "Team member removed successfully"}
|
||||
|
||||
|
||||
@router.get("/roles")
|
||||
def get_team_roles(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get available roles for vendor team."""
|
||||
return team_service.get_vendor_roles(db, vendor.id)
|
||||
|
||||
330
app/api/v1/vendor/vendor.py
vendored
330
app/api/v1/vendor/vendor.py
vendored
@@ -1,330 +0,0 @@
|
||||
# app/api/v1/vendor/vendor.py
|
||||
"""
|
||||
Vendor management endpoints for vendor-scoped operations.
|
||||
|
||||
This module provides:
|
||||
- Vendor profile management
|
||||
- Vendor settings configuration
|
||||
- Vendor team member management
|
||||
- Vendor dashboard statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
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.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.services.team_service import team_service
|
||||
from models.schema.vendor import VendorUpdate, VendorResponse
|
||||
from models.schema.product import ProductResponse, ProductListResponse
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR PROFILE ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/profile", response_model=VendorResponse)
|
||||
def get_vendor_profile(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
return vendor
|
||||
|
||||
|
||||
@router.put("/profile", response_model=VendorResponse)
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
# Verify user has permission to update vendor
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
return vendor_service.update_vendor(db, vendor.id, vendor_update)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR SETTINGS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/settings")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
return {
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"contact_email": vendor.contact_email,
|
||||
"contact_phone": vendor.contact_phone,
|
||||
"website": vendor.website,
|
||||
"business_address": vendor.business_address,
|
||||
"tax_number": vendor.tax_number,
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
"theme_config": vendor.theme_config,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings/marketplace")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Verify permissions
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# Update Letzshop URLs
|
||||
if "letzshop_csv_url_fr" in marketplace_config:
|
||||
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
||||
if "letzshop_csv_url_en" in marketplace_config:
|
||||
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
||||
if "letzshop_csv_url_de" in marketplace_config:
|
||||
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
||||
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Marketplace settings updated successfully",
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings/theme")
|
||||
def update_theme_settings(
|
||||
theme_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor theme configuration."""
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
vendor.theme_config = theme_config
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Theme settings updated successfully",
|
||||
"theme_config": vendor.theme_config,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR CATALOG ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/products", response_model=ProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all products in vendor catalog."""
|
||||
products, total = vendor_service.get_products(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
current_user=current_user,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
active_only=is_active,
|
||||
featured_only=is_featured,
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=products,
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.post("/products", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
product_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Add a product from marketplace to vendor catalog."""
|
||||
from models.schema.product import ProductCreate
|
||||
|
||||
product_create = ProductCreate(**product_data)
|
||||
return vendor_service.add_product_to_catalog(db, vendor, product_create)
|
||||
|
||||
|
||||
@router.get("/products/{product_id}", response_model=ProductResponse)
|
||||
def get_vendor_product(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a specific product from vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
return product_service.get_product(db, vendor.id, product_id)
|
||||
|
||||
|
||||
@router.put("/products/{product_id}", response_model=ProductResponse)
|
||||
def update_vendor_product(
|
||||
product_id: int,
|
||||
product_update: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
from models.schema.product import ProductUpdate
|
||||
|
||||
product_update_schema = ProductUpdate(**product_update)
|
||||
return product_service.update_product(db, vendor.id, product_id, product_update_schema)
|
||||
|
||||
|
||||
@router.delete("/products/{product_id}")
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
product_service.delete_product(db, vendor.id, product_id)
|
||||
return {"message": "Product removed from catalog successfully"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR TEAM ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/team/members")
|
||||
def get_team_members(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all team members for vendor."""
|
||||
return team_service.get_team_members(db, vendor.id, current_user)
|
||||
|
||||
|
||||
@router.post("/team/invite")
|
||||
def invite_team_member(
|
||||
invitation_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Invite a new team member."""
|
||||
return team_service.invite_team_member(db, vendor.id, invitation_data, current_user)
|
||||
|
||||
|
||||
@router.put("/team/members/{user_id}")
|
||||
def update_team_member(
|
||||
user_id: int,
|
||||
update_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update team member role or status."""
|
||||
return team_service.update_team_member(db, vendor.id, user_id, update_data, current_user)
|
||||
|
||||
|
||||
@router.delete("/team/members/{user_id}")
|
||||
def remove_team_member(
|
||||
user_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove team member from vendor."""
|
||||
team_service.remove_team_member(db, vendor.id, user_id, current_user)
|
||||
return {"message": "Team member removed successfully"}
|
||||
|
||||
|
||||
@router.get("/team/roles")
|
||||
def get_team_roles(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get available roles for vendor team."""
|
||||
return team_service.get_vendor_roles(db, vendor.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR DASHBOARD & STATISTICS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/dashboard")
|
||||
def get_vendor_dashboard(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor dashboard statistics."""
|
||||
from app.services.stats_service import stats_service
|
||||
|
||||
return {
|
||||
"vendor": {
|
||||
"code": vendor.vendor_code,
|
||||
"name": vendor.name,
|
||||
"subdomain": vendor.subdomain,
|
||||
"is_verified": vendor.is_verified,
|
||||
},
|
||||
"stats": stats_service.get_vendor_stats(db, vendor.id),
|
||||
"recent_imports": [], # TODO: Implement
|
||||
"recent_orders": [], # TODO: Implement
|
||||
"low_stock_products": [], # TODO: Implement
|
||||
}
|
||||
|
||||
|
||||
@router.get("/analytics")
|
||||
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),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data."""
|
||||
from app.services.stats_service import stats_service
|
||||
|
||||
return stats_service.get_vendor_analytics(db, vendor.id, period)
|
||||
@@ -3,9 +3,10 @@
|
||||
Frontend HTML route handlers.
|
||||
|
||||
Serves static HTML files for admin, vendor, and customer interfaces.
|
||||
Supports both path-based (/vendor/{vendor_code}/) and query-based access.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Path
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
router = APIRouter(include_in_schema=False)
|
||||
@@ -33,60 +34,84 @@ async def admin_vendors():
|
||||
"""Serve admin vendors management page"""
|
||||
return FileResponse("static/admin/vendors.html")
|
||||
|
||||
@router.get("/admin/vendor-edit")
|
||||
async def admin_vendor_edit():
|
||||
"""Serve admin vendor edit page"""
|
||||
return FileResponse("static/admin/vendor-edit.html")
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR ROUTES
|
||||
# VENDOR ROUTES (with vendor code in path)
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/vendor/{vendor_code}/")
|
||||
@router.get("/vendor/{vendor_code}/login")
|
||||
async def vendor_login_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor login page with vendor code in path"""
|
||||
return FileResponse("static/vendor/login.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/dashboard")
|
||||
async def vendor_dashboard_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor dashboard page with vendor code in path"""
|
||||
return FileResponse("static/vendor/dashboard.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/products")
|
||||
@router.get("/vendor/{vendor_code}/admin/products")
|
||||
async def vendor_products_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor products management page"""
|
||||
return FileResponse("static/vendor/admin/products.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/orders")
|
||||
@router.get("/vendor/{vendor_code}/admin/orders")
|
||||
async def vendor_orders_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor orders management page"""
|
||||
return FileResponse("static/vendor/admin/orders.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/marketplace")
|
||||
@router.get("/vendor/{vendor_code}/admin/marketplace")
|
||||
async def vendor_marketplace_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor marketplace import page"""
|
||||
return FileResponse("static/vendor/admin/marketplace.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/customers")
|
||||
@router.get("/vendor/{vendor_code}/admin/customers")
|
||||
async def vendor_customers_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor customers management page"""
|
||||
return FileResponse("static/vendor/admin/customers.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/inventory")
|
||||
@router.get("/vendor/{vendor_code}/admin/inventory")
|
||||
async def vendor_inventory_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor inventory management page"""
|
||||
return FileResponse("static/vendor/admin/inventory.html")
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/team")
|
||||
@router.get("/vendor/{vendor_code}/admin/team")
|
||||
async def vendor_team_with_code(vendor_code: str = Path(...)):
|
||||
"""Serve vendor team management page"""
|
||||
return FileResponse("static/vendor/admin/team.html")
|
||||
|
||||
|
||||
# Fallback vendor routes (without vendor code - for query parameter access)
|
||||
@router.get("/vendor/")
|
||||
@router.get("/vendor/login")
|
||||
async def vendor_login():
|
||||
"""Serve vendor login page"""
|
||||
"""Serve vendor login page (query parameter based)"""
|
||||
return FileResponse("static/vendor/login.html")
|
||||
|
||||
|
||||
@router.get("/vendor/dashboard")
|
||||
async def vendor_dashboard():
|
||||
"""Serve vendor dashboard page"""
|
||||
"""Serve vendor dashboard page (query parameter based)"""
|
||||
return FileResponse("static/vendor/dashboard.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/products")
|
||||
async def vendor_products():
|
||||
"""Serve vendor products management page"""
|
||||
return FileResponse("static/vendor/admin/products.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/orders")
|
||||
async def vendor_orders():
|
||||
"""Serve vendor orders management page"""
|
||||
return FileResponse("static/vendor/admin/orders.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/marketplace")
|
||||
async def vendor_marketplace():
|
||||
"""Serve vendor marketplace import page"""
|
||||
return FileResponse("static/vendor/admin/marketplace.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/customers")
|
||||
async def vendor_customers():
|
||||
"""Serve vendor customers management page"""
|
||||
return FileResponse("static/vendor/admin/customers.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/inventory")
|
||||
async def vendor_inventory():
|
||||
"""Serve vendor inventory management page"""
|
||||
return FileResponse("static/vendor/admin/inventory.html")
|
||||
|
||||
|
||||
@router.get("/vendor/admin/team")
|
||||
async def vendor_team():
|
||||
"""Serve vendor team management page"""
|
||||
return FileResponse("static/vendor/admin/team.html")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CUSTOMER/SHOP ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@@ -31,6 +31,7 @@ class UserRegister(BaseModel):
|
||||
class UserLogin(BaseModel):
|
||||
username: str = Field(..., description="Username")
|
||||
password: str = Field(..., description="Password")
|
||||
vendor_code: Optional[str] = Field(None, description="Optional vendor code for context")
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
|
||||
@@ -1,47 +1,86 @@
|
||||
# models/schema/vendor.py
|
||||
"""
|
||||
Pydantic schemas for Vendor-related operations.
|
||||
|
||||
Schemas include:
|
||||
- VendorCreate: For creating vendors with owner accounts
|
||||
- VendorUpdate: For updating vendor information (Admin only)
|
||||
- VendorResponse: Standard vendor response
|
||||
- VendorDetailResponse: Vendor response with owner details
|
||||
- VendorCreateResponse: Response after vendor creation (includes credentials)
|
||||
- VendorListResponse: Paginated vendor list
|
||||
- VendorSummary: Lightweight vendor info
|
||||
- VendorTransferOwnership: For transferring vendor ownership
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class VendorCreate(BaseModel):
|
||||
"""Schema for creating a new vendor."""
|
||||
vendor_code: str = Field(..., description="Unique vendor identifier (e.g., TECHSTORE)")
|
||||
subdomain: str = Field(..., description="Unique subdomain for the vendor")
|
||||
name: str = Field(..., description="Display name of the vendor")
|
||||
description: Optional[str] = None
|
||||
"""Schema for creating a new vendor with owner account."""
|
||||
|
||||
# Owner information - REQUIRED for admin creation
|
||||
owner_email: str = Field(..., description="Email for the vendor owner account")
|
||||
# Basic Information
|
||||
vendor_code: str = Field(
|
||||
...,
|
||||
description="Unique vendor identifier (e.g., TECHSTORE)",
|
||||
min_length=2,
|
||||
max_length=50
|
||||
)
|
||||
subdomain: str = Field(
|
||||
...,
|
||||
description="Unique subdomain for the vendor",
|
||||
min_length=2,
|
||||
max_length=100
|
||||
)
|
||||
name: str = Field(
|
||||
...,
|
||||
description="Display name of the vendor",
|
||||
min_length=2,
|
||||
max_length=255
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Vendor description")
|
||||
|
||||
# Contact information
|
||||
contact_phone: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
# Owner Information (Creates User Account)
|
||||
owner_email: str = Field(
|
||||
...,
|
||||
description="Email for the vendor owner (used for login and authentication)"
|
||||
)
|
||||
|
||||
# Business information
|
||||
business_address: Optional[str] = None
|
||||
tax_number: Optional[str] = None
|
||||
# Business Contact Information (Vendor Fields)
|
||||
contact_email: Optional[str] = Field(
|
||||
None,
|
||||
description="Public business contact email (defaults to owner_email if not provided)"
|
||||
)
|
||||
contact_phone: Optional[str] = Field(None, description="Contact phone number")
|
||||
website: Optional[str] = Field(None, description="Website URL")
|
||||
|
||||
# Letzshop CSV URLs (multi-language support)
|
||||
letzshop_csv_url_fr: Optional[str] = None
|
||||
letzshop_csv_url_en: Optional[str] = None
|
||||
letzshop_csv_url_de: Optional[str] = None
|
||||
# Business Details
|
||||
business_address: Optional[str] = Field(None, description="Business address")
|
||||
tax_number: Optional[str] = Field(None, description="Tax/VAT number")
|
||||
|
||||
# Theme configuration
|
||||
theme_config: Optional[Dict] = Field(default_factory=dict)
|
||||
# Marketplace URLs (multi-language support)
|
||||
letzshop_csv_url_fr: Optional[str] = Field(None, description="French CSV URL")
|
||||
letzshop_csv_url_en: Optional[str] = Field(None, description="English CSV URL")
|
||||
letzshop_csv_url_de: Optional[str] = Field(None, description="German CSV URL")
|
||||
|
||||
@field_validator("owner_email")
|
||||
# Theme Configuration
|
||||
theme_config: Optional[Dict] = Field(default_factory=dict, description="Theme settings")
|
||||
|
||||
@field_validator("owner_email", "contact_email")
|
||||
@classmethod
|
||||
def validate_owner_email(cls, v):
|
||||
if not v or "@" not in v or "." not in v:
|
||||
raise ValueError("Valid email address required for vendor owner")
|
||||
return v.lower()
|
||||
def validate_emails(cls, v):
|
||||
"""Validate email format and normalize to lowercase."""
|
||||
if v and ("@" not in v or "." not in v):
|
||||
raise ValueError("Invalid email format")
|
||||
return v.lower() if v else v
|
||||
|
||||
@field_validator("subdomain")
|
||||
@classmethod
|
||||
def validate_subdomain(cls, v):
|
||||
# Basic subdomain validation: lowercase alphanumeric with hyphens
|
||||
"""Validate subdomain format: lowercase alphanumeric with hyphens."""
|
||||
if v and not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', v):
|
||||
raise ValueError("Subdomain must contain only lowercase letters, numbers, and hyphens")
|
||||
return v.lower() if v else v
|
||||
@@ -49,35 +88,66 @@ class VendorCreate(BaseModel):
|
||||
@field_validator("vendor_code")
|
||||
@classmethod
|
||||
def validate_vendor_code(cls, v):
|
||||
# Ensure vendor code is uppercase for consistency
|
||||
"""Ensure vendor code is uppercase for consistency."""
|
||||
return v.upper() if v else v
|
||||
|
||||
|
||||
class VendorUpdate(BaseModel):
|
||||
"""Schema for updating vendor information."""
|
||||
name: Optional[str] = None
|
||||
"""
|
||||
Schema for updating vendor information (Admin only).
|
||||
|
||||
Note: owner_email is NOT included here. To change the owner,
|
||||
use the transfer-ownership endpoint instead.
|
||||
"""
|
||||
|
||||
# Basic Information
|
||||
name: Optional[str] = Field(None, min_length=2, max_length=255)
|
||||
description: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
subdomain: Optional[str] = Field(None, min_length=2, max_length=100)
|
||||
|
||||
# Business Contact Information (Vendor Fields)
|
||||
contact_email: Optional[str] = Field(
|
||||
None,
|
||||
description="Public business contact email"
|
||||
)
|
||||
contact_phone: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
|
||||
# Business Details
|
||||
business_address: Optional[str] = None
|
||||
tax_number: Optional[str] = None
|
||||
|
||||
# Marketplace URLs
|
||||
letzshop_csv_url_fr: Optional[str] = None
|
||||
letzshop_csv_url_en: Optional[str] = None
|
||||
letzshop_csv_url_de: Optional[str] = None
|
||||
|
||||
# Theme Configuration
|
||||
theme_config: Optional[Dict] = None
|
||||
|
||||
# Status (Admin only)
|
||||
is_active: Optional[bool] = None
|
||||
is_verified: Optional[bool] = None
|
||||
|
||||
@field_validator("subdomain")
|
||||
@classmethod
|
||||
def subdomain_lowercase(cls, v):
|
||||
"""Normalize subdomain to lowercase."""
|
||||
return v.lower().strip() if v else v
|
||||
|
||||
@field_validator("contact_email")
|
||||
@classmethod
|
||||
def validate_contact_email(cls, v):
|
||||
"""Validate contact email format."""
|
||||
if v and ("@" not in v or "." not in v):
|
||||
raise ValueError("Invalid email format")
|
||||
return v.lower() if v else v
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VendorResponse(BaseModel):
|
||||
"""Schema for vendor response data."""
|
||||
"""Standard schema for vendor response data."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
@@ -87,24 +157,24 @@ class VendorResponse(BaseModel):
|
||||
description: Optional[str]
|
||||
owner_user_id: int
|
||||
|
||||
# Contact information
|
||||
# Contact Information (Business)
|
||||
contact_email: Optional[str]
|
||||
contact_phone: Optional[str]
|
||||
website: Optional[str]
|
||||
|
||||
# Business information
|
||||
# Business Information
|
||||
business_address: Optional[str]
|
||||
tax_number: Optional[str]
|
||||
|
||||
# Letzshop URLs
|
||||
# Marketplace URLs
|
||||
letzshop_csv_url_fr: Optional[str]
|
||||
letzshop_csv_url_en: Optional[str]
|
||||
letzshop_csv_url_de: Optional[str]
|
||||
|
||||
# Theme configuration
|
||||
# Theme Configuration
|
||||
theme_config: Dict
|
||||
|
||||
# Status flags
|
||||
# Status Flags
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
|
||||
@@ -113,6 +183,42 @@ class VendorResponse(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class VendorDetailResponse(VendorResponse):
|
||||
"""
|
||||
Extended vendor response including owner information.
|
||||
|
||||
Includes both:
|
||||
- contact_email (business contact)
|
||||
- owner_email (owner's authentication email)
|
||||
"""
|
||||
|
||||
owner_email: str = Field(
|
||||
...,
|
||||
description="Email of the vendor owner (for login/authentication)"
|
||||
)
|
||||
owner_username: str = Field(
|
||||
...,
|
||||
description="Username of the vendor owner"
|
||||
)
|
||||
|
||||
|
||||
class VendorCreateResponse(VendorDetailResponse):
|
||||
"""
|
||||
Response after creating vendor - includes generated credentials.
|
||||
|
||||
IMPORTANT: temporary_password is shown only once!
|
||||
"""
|
||||
|
||||
temporary_password: str = Field(
|
||||
...,
|
||||
description="Temporary password for owner (SHOWN ONLY ONCE)"
|
||||
)
|
||||
login_url: Optional[str] = Field(
|
||||
None,
|
||||
description="URL for vendor owner to login"
|
||||
)
|
||||
|
||||
|
||||
class VendorListResponse(BaseModel):
|
||||
"""Schema for paginated vendor list."""
|
||||
vendors: List[VendorResponse]
|
||||
@@ -132,9 +238,57 @@ class VendorSummary(BaseModel):
|
||||
is_active: bool
|
||||
|
||||
|
||||
class VendorCreateResponse(VendorResponse):
|
||||
"""Extended response for vendor creation with owner credentials."""
|
||||
owner_email: str
|
||||
owner_username: str
|
||||
temporary_password: str
|
||||
login_url: Optional[str] = None
|
||||
class VendorTransferOwnership(BaseModel):
|
||||
"""
|
||||
Schema for transferring vendor ownership to another user.
|
||||
|
||||
This is a critical operation that requires:
|
||||
- Confirmation flag
|
||||
- Reason for audit trail (optional)
|
||||
"""
|
||||
|
||||
new_owner_user_id: int = Field(
|
||||
...,
|
||||
description="ID of the user who will become the new owner",
|
||||
gt=0
|
||||
)
|
||||
|
||||
confirm_transfer: bool = Field(
|
||||
...,
|
||||
description="Must be true to confirm ownership transfer"
|
||||
)
|
||||
|
||||
transfer_reason: Optional[str] = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="Reason for ownership transfer (for audit logs)"
|
||||
)
|
||||
|
||||
@field_validator("confirm_transfer")
|
||||
@classmethod
|
||||
def validate_confirmation(cls, v):
|
||||
"""Ensure confirmation is explicitly true."""
|
||||
if not v:
|
||||
raise ValueError("Ownership transfer requires explicit confirmation")
|
||||
return v
|
||||
|
||||
|
||||
class VendorTransferOwnershipResponse(BaseModel):
|
||||
"""Response after successful ownership transfer."""
|
||||
|
||||
message: str
|
||||
vendor_id: int
|
||||
vendor_code: str
|
||||
vendor_name: str
|
||||
|
||||
old_owner: Dict[str, Any] = Field(
|
||||
...,
|
||||
description="Information about the previous owner"
|
||||
)
|
||||
new_owner: Dict[str, Any] = Field(
|
||||
...,
|
||||
description="Information about the new owner"
|
||||
)
|
||||
|
||||
transferred_at: datetime
|
||||
transfer_reason: Optional[str]
|
||||
|
||||
Reference in New Issue
Block a user