major refactoring adding vendor and customer features
This commit is contained in:
@@ -4,27 +4,35 @@ Admin service for managing users, vendors, and import jobs.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- User management and status control
|
||||
- Vendor creation with owner user generation
|
||||
- Vendor verification and activation
|
||||
- Marketplace import job monitoring
|
||||
- Platform statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
from app.exceptions import (
|
||||
UserNotFoundException,
|
||||
UserStatusChangeException,
|
||||
CannotModifySelfException,
|
||||
VendorNotFoundException,
|
||||
VendorAlreadyExistsException,
|
||||
VendorVerificationException,
|
||||
AdminOperationException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.schemas.vendor import VendorCreate
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.vendor import Vendor, Role
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,6 +41,10 @@ logger = logging.getLogger(__name__)
|
||||
class AdminService:
|
||||
"""Service class for admin operations following the application's service pattern."""
|
||||
|
||||
# ============================================================================
|
||||
# USER MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
def get_all_users(self, db: Session, skip: int = 0, limit: int = 100) -> List[User]:
|
||||
"""Get paginated list of all users."""
|
||||
try:
|
||||
@@ -47,29 +59,14 @@ class AdminService:
|
||||
def toggle_user_status(
|
||||
self, db: Session, user_id: int, current_admin_id: int
|
||||
) -> Tuple[User, str]:
|
||||
"""
|
||||
Toggle user active status.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
user_id: ID of user to toggle
|
||||
current_admin_id: ID of the admin performing the action
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_user, status_message)
|
||||
|
||||
Raises:
|
||||
UserNotFoundException: If user not found
|
||||
CannotModifySelfException: If trying to modify own account
|
||||
UserStatusChangeException: If status change is not allowed
|
||||
"""
|
||||
"""Toggle user active status."""
|
||||
user = self._get_user_by_id_or_raise(db, user_id)
|
||||
|
||||
# Prevent self-modification
|
||||
if user.id == current_admin_id:
|
||||
raise CannotModifySelfException(user_id, "deactivate account")
|
||||
|
||||
# Check if user is another admin - FIXED LOGIC
|
||||
# Check if user is another admin
|
||||
if user.role == "admin" and user.id != current_admin_id:
|
||||
raise UserStatusChangeException(
|
||||
user_id=user_id,
|
||||
@@ -101,23 +98,150 @@ class AdminService:
|
||||
reason="Database update failed"
|
||||
)
|
||||
|
||||
def get_all_vendors(
|
||||
self, db: Session, skip: int = 0, limit: int = 100
|
||||
) -> Tuple[List[Vendor], int]:
|
||||
# ============================================================================
|
||||
# VENDOR MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
def create_vendor_with_owner(
|
||||
self, db: Session, vendor_data: VendorCreate
|
||||
) -> Tuple[Vendor, User, str]:
|
||||
"""
|
||||
Get paginated list of all vendors with total count.
|
||||
Create vendor with owner user account.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
Tuple of (vendors_list, total_count)
|
||||
Returns: (vendor, owner_user, temporary_password)
|
||||
"""
|
||||
try:
|
||||
total = db.query(Vendor).count()
|
||||
vendors =db.query(Vendor).offset(skip).limit(limit).all()
|
||||
# Check if vendor code already exists
|
||||
existing_vendor = db.query(Vendor).filter(
|
||||
func.upper(Vendor.vendor_code) == vendor_data.vendor_code.upper()
|
||||
).first()
|
||||
|
||||
if existing_vendor:
|
||||
raise VendorAlreadyExistsException(vendor_data.vendor_code)
|
||||
|
||||
# Check if subdomain already exists
|
||||
existing_subdomain = db.query(Vendor).filter(
|
||||
func.lower(Vendor.subdomain) == vendor_data.subdomain.lower()
|
||||
).first()
|
||||
|
||||
if existing_subdomain:
|
||||
raise ValidationException(
|
||||
f"Subdomain '{vendor_data.subdomain}' is already taken"
|
||||
)
|
||||
|
||||
# Generate temporary password for owner
|
||||
temp_password = self._generate_temp_password()
|
||||
|
||||
# Create owner user
|
||||
from middleware.auth import AuthManager
|
||||
auth_manager = AuthManager()
|
||||
|
||||
owner_username = f"{vendor_data.vendor_code.lower()}_owner"
|
||||
owner_email = vendor_data.owner_email if hasattr(vendor_data,
|
||||
'owner_email') else f"{owner_username}@{vendor_data.subdomain}.com"
|
||||
|
||||
# Check if user with this email already exists
|
||||
existing_user = db.query(User).filter(
|
||||
User.email == owner_email
|
||||
).first()
|
||||
|
||||
if existing_user:
|
||||
# Use existing user as owner
|
||||
owner_user = existing_user
|
||||
else:
|
||||
# Create new owner user
|
||||
owner_user = User(
|
||||
email=owner_email,
|
||||
username=owner_username,
|
||||
hashed_password=auth_manager.hash_password(temp_password),
|
||||
role="user", # Will be vendor owner through relationship
|
||||
is_active=True,
|
||||
)
|
||||
db.add(owner_user)
|
||||
db.flush() # Get owner_user.id
|
||||
|
||||
# Create vendor
|
||||
vendor = Vendor(
|
||||
vendor_code=vendor_data.vendor_code.upper(),
|
||||
subdomain=vendor_data.subdomain.lower(),
|
||||
name=vendor_data.name,
|
||||
description=getattr(vendor_data, 'description', None),
|
||||
owner_user_id=owner_user.id,
|
||||
contact_email=owner_email,
|
||||
contact_phone=getattr(vendor_data, 'contact_phone', None),
|
||||
website=getattr(vendor_data, 'website', None),
|
||||
business_address=getattr(vendor_data, 'business_address', None),
|
||||
tax_number=getattr(vendor_data, 'tax_number', None),
|
||||
letzshop_csv_url_fr=getattr(vendor_data, 'letzshop_csv_url_fr', None),
|
||||
letzshop_csv_url_en=getattr(vendor_data, 'letzshop_csv_url_en', None),
|
||||
letzshop_csv_url_de=getattr(vendor_data, 'letzshop_csv_url_de', None),
|
||||
theme_config=getattr(vendor_data, 'theme_config', {}),
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(vendor)
|
||||
db.flush() # Get vendor.id
|
||||
|
||||
# Create default roles for vendor
|
||||
self._create_default_roles(db, vendor.id)
|
||||
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
db.refresh(owner_user)
|
||||
|
||||
logger.info(
|
||||
f"Vendor {vendor.vendor_code} created with owner {owner_user.username}"
|
||||
)
|
||||
|
||||
# TODO: Send welcome email to owner with credentials
|
||||
# self._send_vendor_welcome_email(owner_user, vendor, temp_password)
|
||||
|
||||
return vendor, owner_user, temp_password
|
||||
|
||||
except (VendorAlreadyExistsException, ValidationException):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to create vendor: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="create_vendor_with_owner",
|
||||
reason=f"Failed to create vendor: {str(e)}"
|
||||
)
|
||||
|
||||
def get_all_vendors(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_verified: Optional[bool] = None
|
||||
) -> Tuple[List[Vendor], int]:
|
||||
"""Get paginated list of all vendors with filtering."""
|
||||
try:
|
||||
query = db.query(Vendor)
|
||||
|
||||
# Apply search filter
|
||||
if search:
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Vendor.name.ilike(search_term),
|
||||
Vendor.vendor_code.ilike(search_term),
|
||||
Vendor.subdomain.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
# Apply status filters
|
||||
if is_active is not None:
|
||||
query = query.filter(Vendor.is_active == is_active)
|
||||
if is_verified is not None:
|
||||
query = query.filter(Vendor.is_verified == is_verified)
|
||||
|
||||
total = query.count()
|
||||
vendors = query.offset(skip).limit(limit).all()
|
||||
|
||||
return vendors, total
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve vendors: {str(e)}")
|
||||
@@ -126,21 +250,12 @@ class AdminService:
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_vendor_by_id(self, db: Session, vendor_id: int) -> Vendor:
|
||||
"""Get vendor by ID."""
|
||||
return self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
def verify_vendor(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
|
||||
"""
|
||||
Toggle vendor verification status.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: ID of vendor to verify/unverify
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_vendor, status_message)
|
||||
|
||||
Raises:
|
||||
VendorNotFoundException: If vendor not found
|
||||
VendorVerificationException: If verification fails
|
||||
"""
|
||||
"""Toggle vendor verification status."""
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
try:
|
||||
@@ -148,7 +263,6 @@ class AdminService:
|
||||
vendor.is_verified = not vendor.is_verified
|
||||
vendor.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# Add verification timestamp if implementing audit trail
|
||||
if vendor.is_verified:
|
||||
vendor.verified_at = datetime.now(timezone.utc)
|
||||
|
||||
@@ -171,20 +285,7 @@ class AdminService:
|
||||
)
|
||||
|
||||
def toggle_vendor_status(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
|
||||
"""
|
||||
Toggle vendor active status.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: ID of vendor to activate/deactivate
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_vendor, status_message)
|
||||
|
||||
Raises:
|
||||
VendorNotFoundException: If vendor not found
|
||||
AdminOperationException: If status change fails
|
||||
"""
|
||||
"""Toggle vendor active status."""
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
try:
|
||||
@@ -198,7 +299,7 @@ class AdminService:
|
||||
message = f"Vendor {vendor.vendor_code} has been {status_action}"
|
||||
|
||||
logger.info(message)
|
||||
return vendor , message
|
||||
return vendor, message
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
@@ -210,6 +311,39 @@ class AdminService:
|
||||
target_id=str(vendor_id)
|
||||
)
|
||||
|
||||
def delete_vendor(self, db: Session, vendor_id: int) -> str:
|
||||
"""Delete vendor and all associated data."""
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
try:
|
||||
vendor_code = vendor.vendor_code
|
||||
|
||||
# TODO: Delete associated data in correct order
|
||||
# - Delete orders
|
||||
# - Delete customers
|
||||
# - Delete products
|
||||
# - Delete team members
|
||||
# - Delete roles
|
||||
# - Delete import jobs
|
||||
|
||||
db.delete(vendor)
|
||||
db.commit()
|
||||
|
||||
logger.warning(f"Vendor {vendor_code} and all associated data deleted")
|
||||
return f"Vendor {vendor_code} successfully deleted"
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to delete vendor {vendor_id}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="delete_vendor",
|
||||
reason="Database deletion failed"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# MARKETPLACE IMPORT JOBS
|
||||
# ============================================================================
|
||||
|
||||
def get_marketplace_import_jobs(
|
||||
self,
|
||||
db: Session,
|
||||
@@ -219,24 +353,10 @@ class AdminService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[MarketplaceImportJobResponse]:
|
||||
"""
|
||||
Get filtered and paginated marketplace import jobs.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
marketplace: Filter by marketplace name (case-insensitive partial match)
|
||||
vendor_name: Filter by vendor name (case-insensitive partial match)
|
||||
status: Filter by exact status
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List of MarketplaceImportJobResponse objects
|
||||
"""
|
||||
"""Get filtered and paginated marketplace import jobs."""
|
||||
try:
|
||||
query = db.query(MarketplaceImportJob)
|
||||
|
||||
# Apply filters
|
||||
if marketplace:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
@@ -246,7 +366,6 @@ class AdminService:
|
||||
if status:
|
||||
query = query.filter(MarketplaceImportJob.status == status)
|
||||
|
||||
# Order by creation date and apply pagination
|
||||
jobs = (
|
||||
query.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.offset(skip)
|
||||
@@ -263,17 +382,23 @@ class AdminService:
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# STATISTICS
|
||||
# ============================================================================
|
||||
|
||||
def get_user_statistics(self, db: Session) -> dict:
|
||||
"""Get user statistics for admin dashboard."""
|
||||
try:
|
||||
total_users = db.query(User).count()
|
||||
active_users = db.query(User).filter(User.is_active == True).count()
|
||||
inactive_users = total_users - active_users
|
||||
admin_users = db.query(User).filter(User.role == "admin").count()
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"active_users": active_users,
|
||||
"inactive_users": inactive_users,
|
||||
"admin_users": admin_users,
|
||||
"activation_rate": (active_users / total_users * 100) if total_users > 0 else 0
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -289,10 +414,12 @@ class AdminService:
|
||||
total_vendors = db.query(Vendor).count()
|
||||
active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count()
|
||||
verified_vendors = db.query(Vendor).filter(Vendor.is_verified == True).count()
|
||||
inactive_vendors = total_vendors - active_vendors
|
||||
|
||||
return {
|
||||
"total_vendors": total_vendors,
|
||||
"active_vendors": active_vendors,
|
||||
"inactive_vendors": inactive_vendors,
|
||||
"verified_vendors": verified_vendors,
|
||||
"verification_rate": (verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
||||
}
|
||||
@@ -303,7 +430,100 @@ class AdminService:
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
# Private helper methods
|
||||
def get_recent_vendors(self, db: Session, limit: int = 5) -> List[dict]:
|
||||
"""Get recently created vendors."""
|
||||
try:
|
||||
vendors = (
|
||||
db.query(Vendor)
|
||||
.order_by(Vendor.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": v.id,
|
||||
"vendor_code": v.vendor_code,
|
||||
"name": v.name,
|
||||
"subdomain": v.subdomain,
|
||||
"is_active": v.is_active,
|
||||
"is_verified": v.is_verified,
|
||||
"created_at": v.created_at
|
||||
}
|
||||
for v in vendors
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get recent vendors: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_recent_import_jobs(self, db: Session, limit: int = 10) -> List[dict]:
|
||||
"""Get recent marketplace import jobs."""
|
||||
try:
|
||||
jobs = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": j.id,
|
||||
"marketplace": j.marketplace,
|
||||
"vendor_name": j.vendor_name,
|
||||
"status": j.status,
|
||||
"total_processed": j.total_processed or 0,
|
||||
"created_at": j.created_at
|
||||
}
|
||||
for j in jobs
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get recent import jobs: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_product_statistics(self, db: Session) -> dict:
|
||||
"""Get product statistics."""
|
||||
# TODO: Implement when Product model is available
|
||||
return {
|
||||
"total_products": 0,
|
||||
"active_products": 0,
|
||||
"out_of_stock": 0
|
||||
}
|
||||
|
||||
def get_order_statistics(self, db: Session) -> dict:
|
||||
"""Get order statistics."""
|
||||
# TODO: Implement when Order model is available
|
||||
return {
|
||||
"total_orders": 0,
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0
|
||||
}
|
||||
|
||||
def get_import_statistics(self, db: Session) -> dict:
|
||||
"""Get import job statistics."""
|
||||
try:
|
||||
total = db.query(MarketplaceImportJob).count()
|
||||
completed = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.status == "completed"
|
||||
).count()
|
||||
failed = db.query(MarketplaceImportJob).filter(
|
||||
MarketplaceImportJob.status == "failed"
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total_imports": total,
|
||||
"completed_imports": completed,
|
||||
"failed_imports": failed,
|
||||
"success_rate": (completed / total * 100) if total > 0 else 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get import statistics: {str(e)}")
|
||||
return {"total_imports": 0, "completed_imports": 0, "failed_imports": 0, "success_rate": 0}
|
||||
|
||||
# ============================================================================
|
||||
# PRIVATE HELPER METHODS
|
||||
# ============================================================================
|
||||
|
||||
def _get_user_by_id_or_raise(self, db: Session, user_id: int) -> User:
|
||||
"""Get user by ID or raise UserNotFoundException."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
@@ -314,9 +534,52 @@ class AdminService:
|
||||
def _get_vendor_by_id_or_raise(self, db: Session, vendor_id: int) -> Vendor:
|
||||
"""Get vendor by ID or raise VendorNotFoundException."""
|
||||
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
if not vendor :
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
return vendor
|
||||
return vendor
|
||||
|
||||
def _generate_temp_password(self, length: int = 12) -> str:
|
||||
"""Generate secure temporary password."""
|
||||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def _create_default_roles(self, db: Session, vendor_id: int):
|
||||
"""Create default roles for a new vendor."""
|
||||
default_roles = [
|
||||
{
|
||||
"name": "Owner",
|
||||
"permissions": ["*"] # Full access
|
||||
},
|
||||
{
|
||||
"name": "Manager",
|
||||
"permissions": [
|
||||
"products.*", "orders.*", "customers.view",
|
||||
"inventory.*", "team.view"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Editor",
|
||||
"permissions": [
|
||||
"products.view", "products.edit",
|
||||
"orders.view", "inventory.view"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Viewer",
|
||||
"permissions": [
|
||||
"products.view", "orders.view",
|
||||
"customers.view", "inventory.view"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
for role_data in default_roles:
|
||||
role = Role(
|
||||
vendor_id=vendor_id,
|
||||
name=role_data["name"],
|
||||
permissions=role_data["permissions"]
|
||||
)
|
||||
db.add(role)
|
||||
|
||||
def _convert_job_to_response(self, job: MarketplaceImportJob) -> MarketplaceImportJobResponse:
|
||||
"""Convert database model to response schema."""
|
||||
@@ -338,5 +601,5 @@ class AdminService:
|
||||
)
|
||||
|
||||
|
||||
# Create service instance following the same pattern as marketplace_product_service
|
||||
# Create service instance
|
||||
admin_service = AdminService()
|
||||
|
||||
Reference in New Issue
Block a user