vendor features for admin and vendor admin area

This commit is contained in:
2025-10-19 16:03:25 +02:00
parent 06bb463468
commit 9aee314837
15 changed files with 1693 additions and 471 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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