major refactoring adding vendor and customer features

This commit is contained in:
2025-10-11 09:09:25 +02:00
parent f569995883
commit dd16198276
126 changed files with 15109 additions and 3747 deletions

View File

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