shop product refactoring
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# app/services/admin_service.py
|
||||
"""
|
||||
Admin service for managing users, shops, and import jobs.
|
||||
Admin service for managing users, vendors, and import jobs.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- User management and status control
|
||||
- Shop verification and activation
|
||||
- Vendor verification and activation
|
||||
- Marketplace import job monitoring
|
||||
"""
|
||||
|
||||
@@ -18,13 +18,13 @@ from app.exceptions import (
|
||||
UserNotFoundException,
|
||||
UserStatusChangeException,
|
||||
CannotModifySelfException,
|
||||
ShopNotFoundException,
|
||||
ShopVerificationException,
|
||||
VendorNotFoundException,
|
||||
VendorVerificationException,
|
||||
AdminOperationException,
|
||||
)
|
||||
from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.shop import Shop
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -101,11 +101,11 @@ class AdminService:
|
||||
reason="Database update failed"
|
||||
)
|
||||
|
||||
def get_all_shops(
|
||||
def get_all_vendors(
|
||||
self, db: Session, skip: int = 0, limit: int = 100
|
||||
) -> Tuple[List[Shop], int]:
|
||||
) -> Tuple[List[Vendor], int]:
|
||||
"""
|
||||
Get paginated list of all shops with total count.
|
||||
Get paginated list of all vendors with total count.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
@@ -113,108 +113,108 @@ class AdminService:
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
Tuple of (shops_list, total_count)
|
||||
Tuple of (vendors_list, total_count)
|
||||
"""
|
||||
try:
|
||||
total = db.query(Shop).count()
|
||||
shops = db.query(Shop).offset(skip).limit(limit).all()
|
||||
return shops, total
|
||||
total = db.query(Vendor).count()
|
||||
vendors =db.query(Vendor).offset(skip).limit(limit).all()
|
||||
return vendors, total
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve shops: {str(e)}")
|
||||
logger.error(f"Failed to retrieve vendors: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_all_shops",
|
||||
operation="get_all_vendors",
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
def verify_shop(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
|
||||
def verify_vendor(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
|
||||
"""
|
||||
Toggle shop verification status.
|
||||
Toggle vendor verification status.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop_id: ID of shop to verify/unverify
|
||||
vendor_id: ID of vendor to verify/unverify
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_shop, status_message)
|
||||
Tuple of (updated_vendor, status_message)
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop not found
|
||||
ShopVerificationException: If verification fails
|
||||
VendorNotFoundException: If vendor not found
|
||||
VendorVerificationException: If verification fails
|
||||
"""
|
||||
shop = self._get_shop_by_id_or_raise(db, shop_id)
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
try:
|
||||
original_status = shop.is_verified
|
||||
shop.is_verified = not shop.is_verified
|
||||
shop.updated_at = datetime.now(timezone.utc)
|
||||
original_status = vendor.is_verified
|
||||
vendor.is_verified = not vendor.is_verified
|
||||
vendor.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# Add verification timestamp if implementing audit trail
|
||||
if shop.is_verified:
|
||||
shop.verified_at = datetime.now(timezone.utc)
|
||||
if vendor.is_verified:
|
||||
vendor.verified_at = datetime.now(timezone.utc)
|
||||
|
||||
db.commit()
|
||||
db.refresh(shop)
|
||||
db.refresh(vendor)
|
||||
|
||||
status_action = "verified" if shop.is_verified else "unverified"
|
||||
message = f"Shop {shop.shop_code} has been {status_action}"
|
||||
status_action = "verified" if vendor.is_verified else "unverified"
|
||||
message = f"Vendor {vendor.vendor_code} has been {status_action}"
|
||||
|
||||
logger.info(message)
|
||||
return shop, message
|
||||
return vendor, message
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to verify shop {shop_id}: {str(e)}")
|
||||
raise ShopVerificationException(
|
||||
shop_id=shop_id,
|
||||
logger.error(f"Failed to verify vendor {vendor_id}: {str(e)}")
|
||||
raise VendorVerificationException(
|
||||
vendor_id=vendor_id,
|
||||
reason="Database update failed",
|
||||
current_verification_status=original_status
|
||||
)
|
||||
|
||||
def toggle_shop_status(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
|
||||
def toggle_vendor_status(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
|
||||
"""
|
||||
Toggle shop active status.
|
||||
Toggle vendor active status.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop_id: ID of shop to activate/deactivate
|
||||
vendor_id: ID of vendor to activate/deactivate
|
||||
|
||||
Returns:
|
||||
Tuple of (updated_shop, status_message)
|
||||
Tuple of (updated_vendor, status_message)
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop not found
|
||||
VendorNotFoundException: If vendor not found
|
||||
AdminOperationException: If status change fails
|
||||
"""
|
||||
shop = self._get_shop_by_id_or_raise(db, shop_id)
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
try:
|
||||
original_status = shop.is_active
|
||||
shop.is_active = not shop.is_active
|
||||
shop.updated_at = datetime.now(timezone.utc)
|
||||
original_status = vendor.is_active
|
||||
vendor.is_active = not vendor.is_active
|
||||
vendor.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(shop)
|
||||
db.refresh(vendor)
|
||||
|
||||
status_action = "activated" if shop.is_active else "deactivated"
|
||||
message = f"Shop {shop.shop_code} has been {status_action}"
|
||||
status_action = "activated" if vendor.is_active else "deactivated"
|
||||
message = f"Vendor {vendor.vendor_code} has been {status_action}"
|
||||
|
||||
logger.info(message)
|
||||
return shop, message
|
||||
return vendor , message
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to toggle shop {shop_id} status: {str(e)}")
|
||||
logger.error(f"Failed to toggle vendor {vendor_id} status: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="toggle_shop_status",
|
||||
operation="toggle_vendor_status",
|
||||
reason="Database update failed",
|
||||
target_type="shop",
|
||||
target_id=str(shop_id)
|
||||
target_type="vendor ",
|
||||
target_id=str(vendor_id)
|
||||
)
|
||||
|
||||
def get_marketplace_import_jobs(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -225,7 +225,7 @@ class AdminService:
|
||||
Args:
|
||||
db: Database session
|
||||
marketplace: Filter by marketplace name (case-insensitive partial match)
|
||||
shop_name: Filter by shop 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
|
||||
@@ -241,8 +241,8 @@ class AdminService:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
if status:
|
||||
query = query.filter(MarketplaceImportJob.status == status)
|
||||
|
||||
@@ -283,23 +283,23 @@ class AdminService:
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
def get_shop_statistics(self, db: Session) -> dict:
|
||||
"""Get shop statistics for admin dashboard."""
|
||||
def get_vendor_statistics(self, db: Session) -> dict:
|
||||
"""Get vendor statistics for admin dashboard."""
|
||||
try:
|
||||
total_shops = db.query(Shop).count()
|
||||
active_shops = db.query(Shop).filter(Shop.is_active == True).count()
|
||||
verified_shops = db.query(Shop).filter(Shop.is_verified == True).count()
|
||||
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()
|
||||
|
||||
return {
|
||||
"total_shops": total_shops,
|
||||
"active_shops": active_shops,
|
||||
"verified_shops": verified_shops,
|
||||
"verification_rate": (verified_shops / total_shops * 100) if total_shops > 0 else 0
|
||||
"total_vendors": total_vendors,
|
||||
"active_vendors": active_vendors,
|
||||
"verified_vendors": verified_vendors,
|
||||
"verification_rate": (verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get shop statistics: {str(e)}")
|
||||
logger.error(f"Failed to get vendor statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_shop_statistics",
|
||||
operation="get_vendor_statistics",
|
||||
reason="Database query failed"
|
||||
)
|
||||
|
||||
@@ -311,12 +311,12 @@ class AdminService:
|
||||
raise UserNotFoundException(str(user_id))
|
||||
return user
|
||||
|
||||
def _get_shop_by_id_or_raise(self, db: Session, shop_id: int) -> Shop:
|
||||
"""Get shop by ID or raise ShopNotFoundException."""
|
||||
shop = db.query(Shop).filter(Shop.id == shop_id).first()
|
||||
if not shop:
|
||||
raise ShopNotFoundException(str(shop_id), identifier_type="id")
|
||||
return shop
|
||||
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 :
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
return vendor
|
||||
|
||||
def _convert_job_to_response(self, job: MarketplaceImportJob) -> MarketplaceImportJobResponse:
|
||||
"""Convert database model to response schema."""
|
||||
@@ -324,9 +324,9 @@ class AdminService:
|
||||
job_id=job.id,
|
||||
status=job.status,
|
||||
marketplace=job.marketplace,
|
||||
shop_id=job.shop.id if job.shop else None,
|
||||
shop_code=job.shop.shop_code if job.shop else None,
|
||||
shop_name=job.shop_name,
|
||||
vendor_id=job.vendor.id if job.vendor else None,
|
||||
vendor_code=job.vendor.vendor_code if job.vendor else None,
|
||||
vendor_name=job.vendor_name,
|
||||
imported=job.imported_count or 0,
|
||||
updated=job.updated_count or 0,
|
||||
total_processed=job.total_processed or 0,
|
||||
|
||||
@@ -4,7 +4,7 @@ Marketplace service for managing import jobs and marketplace integrations.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Import job creation and management
|
||||
- Shop access validation
|
||||
- Vendor access validation
|
||||
- Import job status tracking and updates
|
||||
"""
|
||||
|
||||
@@ -16,8 +16,8 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ShopNotFoundException,
|
||||
UnauthorizedShopAccessException,
|
||||
VendorNotFoundException,
|
||||
UnauthorizedVendorAccessException,
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException,
|
||||
ImportJobCannotBeCancelledException,
|
||||
@@ -27,7 +27,7 @@ from app.exceptions import (
|
||||
from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobRequest)
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.shop import Shop
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,44 +36,44 @@ logger = logging.getLogger(__name__)
|
||||
class MarketplaceImportJobService:
|
||||
"""Service class for Marketplace operations following the application's service pattern."""
|
||||
|
||||
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
|
||||
def validate_vendor_access(self, db: Session, vendor_code: str, user: User) -> Vendor:
|
||||
"""
|
||||
Validate that the shop exists and user has access to it.
|
||||
Validate that the vendor exists and user has access to it.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop_code: Shop code to validate
|
||||
vendor_code: Vendor code to validate
|
||||
user: User requesting access
|
||||
|
||||
Returns:
|
||||
Shop object if access is valid
|
||||
Vendor object if access is valid
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop doesn't exist
|
||||
UnauthorizedShopAccessException: If user lacks access
|
||||
VendorNotFoundException: If vendor doesn't exist
|
||||
UnauthorizedVendorAccessException: If user lacks access
|
||||
"""
|
||||
try:
|
||||
# Use case-insensitive query to handle both uppercase and lowercase codes
|
||||
shop = (
|
||||
db.query(Shop)
|
||||
.filter(func.upper(Shop.shop_code) == shop_code.upper())
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not shop:
|
||||
raise ShopNotFoundException(shop_code)
|
||||
if not vendor :
|
||||
raise VendorNotFoundException(vendor_code)
|
||||
|
||||
# Check permissions: admin can import for any shop, others only for their own
|
||||
if user.role != "admin" and shop.owner_id != user.id:
|
||||
raise UnauthorizedShopAccessException(shop_code, user.id)
|
||||
# Check permissions: admin can import for any vendor, others only for their own
|
||||
if user.role != "admin" and vendor.owner_id != user.id:
|
||||
raise UnauthorizedVendorAccessException(vendor_code, user.id)
|
||||
|
||||
return shop
|
||||
return vendor
|
||||
|
||||
except (ShopNotFoundException, UnauthorizedShopAccessException):
|
||||
except (VendorNotFoundException, UnauthorizedVendorAccessException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating shop access: {str(e)}")
|
||||
raise ValidationException("Failed to validate shop access")
|
||||
logger.error(f"Error validating vendor access: {str(e)}")
|
||||
raise ValidationException("Failed to validate vendor access")
|
||||
|
||||
def create_import_job(
|
||||
self, db: Session, request: MarketplaceImportJobRequest, user: User
|
||||
@@ -90,21 +90,21 @@ class MarketplaceImportJobService:
|
||||
Created MarketplaceImportJob object
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop doesn't exist
|
||||
UnauthorizedShopAccessException: If user lacks shop access
|
||||
VendorNotFoundException: If vendor doesn't exist
|
||||
UnauthorizedVendorAccessException: If user lacks vendor access
|
||||
ValidationException: If job creation fails
|
||||
"""
|
||||
try:
|
||||
# Validate shop access first
|
||||
shop = self.validate_shop_access(db, request.shop_code, user)
|
||||
# Validate vendor access first
|
||||
vendor = self.validate_vendor_access(db, request.vendor_code, user)
|
||||
|
||||
# Create marketplace import job record
|
||||
import_job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
source_url=request.url,
|
||||
marketplace=request.marketplace,
|
||||
shop_id=shop.id, # Foreign key to shops table
|
||||
shop_name=shop.shop_name, # Use shop.shop_name (the display name)
|
||||
vendor_id=vendor.id, # Foreign key to vendors table
|
||||
vendor_name=vendor.vendor_name, # Use vendor.vendor_name (the display name)
|
||||
user_id=user.id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
@@ -115,12 +115,12 @@ class MarketplaceImportJobService:
|
||||
|
||||
logger.info(
|
||||
f"Created marketplace import job {import_job.id}: "
|
||||
f"{request.marketplace} -> {shop.shop_name} (shop_code: {shop.shop_code}) by user {user.username}"
|
||||
f"{request.marketplace} -> {vendor.vendor_name} (vendor_code: {vendor.vendor_code}) by user {user.username}"
|
||||
)
|
||||
|
||||
return import_job
|
||||
|
||||
except (ShopNotFoundException, UnauthorizedShopAccessException):
|
||||
except (VendorNotFoundException, UnauthorizedVendorAccessException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
@@ -172,7 +172,7 @@ class MarketplaceImportJobService:
|
||||
db: Session,
|
||||
user: User,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[MarketplaceImportJob]:
|
||||
@@ -183,7 +183,7 @@ class MarketplaceImportJobService:
|
||||
db: Database session
|
||||
user: User requesting jobs
|
||||
marketplace: Optional marketplace filter
|
||||
shop_name: Optional shop name filter
|
||||
vendor_name: Optional vendor name filter
|
||||
skip: Number of records to skip
|
||||
limit: Maximum records to return
|
||||
|
||||
@@ -202,8 +202,8 @@ class MarketplaceImportJobService:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
|
||||
# Order by creation date (newest first) and apply pagination
|
||||
jobs = (
|
||||
@@ -319,11 +319,11 @@ class MarketplaceImportJobService:
|
||||
job_id=job.id,
|
||||
status=job.status,
|
||||
marketplace=job.marketplace,
|
||||
shop_id=job.shop_id,
|
||||
shop_code=(
|
||||
job.shop.shop_code if job.shop else None
|
||||
vendor_id=job.vendor_id,
|
||||
vendor_code=(
|
||||
job.vendor.vendor_code if job.vendor else None
|
||||
), # Add this optional field via relationship
|
||||
shop_name=job.shop_name,
|
||||
vendor_name=job.vendor_name,
|
||||
imported=job.imported_count or 0,
|
||||
updated=job.updated_count or 0,
|
||||
total_processed=job.total_processed or 0,
|
||||
|
||||
@@ -135,7 +135,7 @@ class MarketplaceProductService:
|
||||
category: Optional[str] = None,
|
||||
availability: Optional[str] = None,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
) -> Tuple[List[MarketplaceProduct], int]:
|
||||
"""
|
||||
@@ -149,7 +149,7 @@ class MarketplaceProductService:
|
||||
category: Category filter
|
||||
availability: Availability filter
|
||||
marketplace: Marketplace filter
|
||||
shop_name: Shop name filter
|
||||
vendor_name: Vendor name filter
|
||||
search: Search term
|
||||
|
||||
Returns:
|
||||
@@ -167,16 +167,16 @@ class MarketplaceProductService:
|
||||
query = query.filter(MarketplaceProduct.availability == availability)
|
||||
if marketplace:
|
||||
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
if search:
|
||||
# Search in title, description, marketplace, and shop_name
|
||||
# Search in title, description, marketplace, and vendor_name
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(MarketplaceProduct.title.ilike(search_term))
|
||||
| (MarketplaceProduct.description.ilike(search_term))
|
||||
| (MarketplaceProduct.marketplace.ilike(search_term))
|
||||
| (MarketplaceProduct.shop_name.ilike(search_term))
|
||||
| (MarketplaceProduct.vendor_name.ilike(search_term))
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
@@ -311,7 +311,7 @@ class MarketplaceProductService:
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generate CSV export with streaming for memory efficiency and proper CSV escaping.
|
||||
@@ -319,7 +319,7 @@ class MarketplaceProductService:
|
||||
Args:
|
||||
db: Database session
|
||||
marketplace: Optional marketplace filter
|
||||
shop_name: Optional shop name filter
|
||||
vendor_name: Optional vendor name filter
|
||||
|
||||
Yields:
|
||||
CSV content as strings with proper escaping
|
||||
@@ -333,7 +333,7 @@ class MarketplaceProductService:
|
||||
headers = [
|
||||
"marketplace_product_id", "title", "description", "link", "image_link",
|
||||
"availability", "price", "currency", "brand", "gtin",
|
||||
"marketplace", "shop_name"
|
||||
"marketplace", "vendor_name"
|
||||
]
|
||||
writer.writerow(headers)
|
||||
yield output.getvalue()
|
||||
@@ -351,8 +351,8 @@ class MarketplaceProductService:
|
||||
# Apply marketplace filters
|
||||
if marketplace:
|
||||
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
|
||||
if vendor_name:
|
||||
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
|
||||
|
||||
products = query.offset(offset).limit(batch_size).all()
|
||||
if not products:
|
||||
@@ -372,7 +372,7 @@ class MarketplaceProductService:
|
||||
product.brand or "",
|
||||
product.gtin or "",
|
||||
product.marketplace or "",
|
||||
product.shop_name or "",
|
||||
product.vendor_name or "",
|
||||
]
|
||||
|
||||
writer.writerow(row_data)
|
||||
@@ -413,7 +413,7 @@ class MarketplaceProductService:
|
||||
normalized = product_data.copy()
|
||||
|
||||
# Trim whitespace from string fields
|
||||
string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'shop_name']
|
||||
string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'vendor_name']
|
||||
for field in string_fields:
|
||||
if field in normalized and normalized[field]:
|
||||
normalized[field] = normalized[field].strip()
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
# app/services/shop_service.py
|
||||
"""
|
||||
Shop service for managing shop operations and product catalog.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Shop creation and management
|
||||
- Shop access control and validation
|
||||
- Shop product catalog operations
|
||||
- Shop filtering and search
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ShopNotFoundException,
|
||||
ShopAlreadyExistsException,
|
||||
UnauthorizedShopAccessException,
|
||||
InvalidShopDataException,
|
||||
MarketplaceProductNotFoundException,
|
||||
ProductAlreadyExistsException,
|
||||
MaxShopsReachedException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.shop import ShopCreate
|
||||
from models.schemas.product import ProductCreate
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.shop import Shop
|
||||
from models.database.product import Product
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShopService:
|
||||
"""Service class for shop operations following the application's service pattern."""
|
||||
|
||||
def create_shop(
|
||||
self, db: Session, shop_data: ShopCreate, current_user: User
|
||||
) -> Shop:
|
||||
"""
|
||||
Create a new shop.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop_data: Shop creation data
|
||||
current_user: User creating the shop
|
||||
|
||||
Returns:
|
||||
Created shop object
|
||||
|
||||
Raises:
|
||||
ShopAlreadyExistsException: If shop code already exists
|
||||
MaxShopsReachedException: If user has reached maximum shops
|
||||
InvalidShopDataException: If shop data is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate shop data
|
||||
self._validate_shop_data(shop_data)
|
||||
|
||||
# Check user's shop limit (if applicable)
|
||||
self._check_shop_limit(db, current_user)
|
||||
|
||||
# Normalize shop code to uppercase
|
||||
normalized_shop_code = shop_data.shop_code.upper()
|
||||
|
||||
# Check if shop code already exists (case-insensitive check)
|
||||
if self._shop_code_exists(db, normalized_shop_code):
|
||||
raise ShopAlreadyExistsException(normalized_shop_code)
|
||||
|
||||
# Create shop with uppercase code
|
||||
shop_dict = shop_data.model_dump()
|
||||
shop_dict["shop_code"] = normalized_shop_code # Store as uppercase
|
||||
|
||||
new_shop = Shop(
|
||||
**shop_dict,
|
||||
owner_id=current_user.id,
|
||||
is_active=True,
|
||||
is_verified=(current_user.role == "admin"),
|
||||
)
|
||||
|
||||
db.add(new_shop)
|
||||
db.commit()
|
||||
db.refresh(new_shop)
|
||||
|
||||
logger.info(
|
||||
f"New shop created: {new_shop.shop_code} by {current_user.username}"
|
||||
)
|
||||
return new_shop
|
||||
|
||||
except (ShopAlreadyExistsException, MaxShopsReachedException, InvalidShopDataException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating shop: {str(e)}")
|
||||
raise ValidationException("Failed to create shop")
|
||||
|
||||
def get_shops(
|
||||
self,
|
||||
db: Session,
|
||||
current_user: User,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
verified_only: bool = False,
|
||||
) -> Tuple[List[Shop], int]:
|
||||
"""
|
||||
Get shops with filtering.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
current_user: Current user requesting shops
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
active_only: Filter for active shops only
|
||||
verified_only: Filter for verified shops only
|
||||
|
||||
Returns:
|
||||
Tuple of (shops_list, total_count)
|
||||
"""
|
||||
try:
|
||||
query = db.query(Shop)
|
||||
|
||||
# Non-admin users can only see active and verified shops, plus their own
|
||||
if current_user.role != "admin":
|
||||
query = query.filter(
|
||||
(Shop.is_active == True)
|
||||
& ((Shop.is_verified == True) | (Shop.owner_id == current_user.id))
|
||||
)
|
||||
else:
|
||||
# Admin can apply filters
|
||||
if active_only:
|
||||
query = query.filter(Shop.is_active == True)
|
||||
if verified_only:
|
||||
query = query.filter(Shop.is_verified == True)
|
||||
|
||||
total = query.count()
|
||||
shops = query.offset(skip).limit(limit).all()
|
||||
|
||||
return shops, total
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting shops: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve shops")
|
||||
|
||||
def get_shop_by_code(self, db: Session, shop_code: str, current_user: User) -> Shop:
|
||||
"""
|
||||
Get shop by shop code with access control.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop_code: Shop code to find
|
||||
current_user: Current user requesting the shop
|
||||
|
||||
Returns:
|
||||
Shop object
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop not found
|
||||
UnauthorizedShopAccessException: If access denied
|
||||
"""
|
||||
try:
|
||||
shop = (
|
||||
db.query(Shop)
|
||||
.filter(func.upper(Shop.shop_code) == shop_code.upper())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not shop:
|
||||
raise ShopNotFoundException(shop_code)
|
||||
|
||||
# Check access permissions
|
||||
if not self._can_access_shop(shop, current_user):
|
||||
raise UnauthorizedShopAccessException(shop_code, current_user.id)
|
||||
|
||||
return shop
|
||||
|
||||
except (ShopNotFoundException, UnauthorizedShopAccessException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting shop {shop_code}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve shop")
|
||||
|
||||
def add_product_to_shop(
|
||||
self, db: Session, shop: Shop, product: ProductCreate
|
||||
) -> Product:
|
||||
"""
|
||||
Add existing product to shop catalog with shop-specific settings.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop: Shop to add product to
|
||||
product: Shop product data
|
||||
|
||||
Returns:
|
||||
Created ShopProduct object
|
||||
|
||||
Raises:
|
||||
MarketplaceProductNotFoundException: If product not found
|
||||
ProductAlreadyExistsException: If product already in shop
|
||||
"""
|
||||
try:
|
||||
# Check if product exists
|
||||
marketplace_product = self._get_product_by_id_or_raise(db, product.marketplace_product_id)
|
||||
|
||||
# Check if product already in shop
|
||||
if self._product_in_shop(db, shop.id, marketplace_product.id):
|
||||
raise ProductAlreadyExistsException(shop.shop_code, product.marketplace_product_id)
|
||||
|
||||
# Create shop-product association
|
||||
new_product = Product(
|
||||
shop_id=shop.id,
|
||||
marketplace_product_id=marketplace_product.id,
|
||||
**product.model_dump(exclude={"marketplace_product_id"}),
|
||||
)
|
||||
|
||||
db.add(new_product)
|
||||
db.commit()
|
||||
db.refresh(new_product)
|
||||
|
||||
# Load the product relationship
|
||||
db.refresh(new_product)
|
||||
|
||||
logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to shop {shop.shop_code}")
|
||||
return new_product
|
||||
|
||||
except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error adding product to shop: {str(e)}")
|
||||
raise ValidationException("Failed to add product to shop")
|
||||
|
||||
def get_products(
|
||||
self,
|
||||
db: Session,
|
||||
shop: Shop,
|
||||
current_user: User,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
featured_only: bool = False,
|
||||
) -> Tuple[List[Product], int]:
|
||||
"""
|
||||
Get products in shop catalog with filtering.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
shop: Shop to get products from
|
||||
current_user: Current user requesting products
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
active_only: Filter for active products only
|
||||
featured_only: Filter for featured products only
|
||||
|
||||
Returns:
|
||||
Tuple of (products_list, total_count)
|
||||
|
||||
Raises:
|
||||
UnauthorizedShopAccessException: If shop access denied
|
||||
"""
|
||||
try:
|
||||
# Check access permissions
|
||||
if not self._can_access_shop(shop, current_user):
|
||||
raise UnauthorizedShopAccessException(shop.shop_code, current_user.id)
|
||||
|
||||
# Query shop products
|
||||
query = db.query(Product).filter(Product.shop_id == shop.id)
|
||||
|
||||
if active_only:
|
||||
query = query.filter(Product.is_active == True)
|
||||
if featured_only:
|
||||
query = query.filter(Product.is_featured == True)
|
||||
|
||||
total = query.count()
|
||||
products = query.offset(skip).limit(limit).all()
|
||||
|
||||
return products, total
|
||||
|
||||
except UnauthorizedShopAccessException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting shop products: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve shop products")
|
||||
|
||||
# Private helper methods
|
||||
def _validate_shop_data(self, shop_data: ShopCreate) -> None:
|
||||
"""Validate shop creation data."""
|
||||
if not shop_data.shop_code or not shop_data.shop_code.strip():
|
||||
raise InvalidShopDataException("Shop code is required", field="shop_code")
|
||||
|
||||
if not shop_data.shop_name or not shop_data.shop_name.strip():
|
||||
raise InvalidShopDataException("Shop name is required", field="shop_name")
|
||||
|
||||
# Validate shop code format (alphanumeric, underscores, hyphens)
|
||||
import re
|
||||
if not re.match(r'^[A-Za-z0-9_-]+$', shop_data.shop_code):
|
||||
raise InvalidShopDataException(
|
||||
"Shop code can only contain letters, numbers, underscores, and hyphens",
|
||||
field="shop_code"
|
||||
)
|
||||
|
||||
def _check_shop_limit(self, db: Session, user: User) -> None:
|
||||
"""Check if user has reached maximum shop limit."""
|
||||
if user.role == "admin":
|
||||
return # Admins have no limit
|
||||
|
||||
user_shop_count = db.query(Shop).filter(Shop.owner_id == user.id).count()
|
||||
max_shops = 5 # Configure this as needed
|
||||
|
||||
if user_shop_count >= max_shops:
|
||||
raise MaxShopsReachedException(max_shops, user.id)
|
||||
|
||||
def _shop_code_exists(self, db: Session, shop_code: str) -> bool:
|
||||
"""Check if shop code already exists (case-insensitive)."""
|
||||
return (
|
||||
db.query(Shop)
|
||||
.filter(func.upper(Shop.shop_code) == shop_code.upper())
|
||||
.first() is not None
|
||||
)
|
||||
|
||||
def _get_product_by_id_or_raise(self, db: Session, marketplace_product_id: str) -> MarketplaceProduct:
|
||||
"""Get product by ID or raise exception."""
|
||||
product = db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
|
||||
if not product:
|
||||
raise MarketplaceProductNotFoundException(marketplace_product_id)
|
||||
return product
|
||||
|
||||
def _product_in_shop(self, db: Session, shop_id: int, marketplace_product_id: int) -> bool:
|
||||
"""Check if product is already in shop."""
|
||||
return (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.shop_id == shop_id,
|
||||
Product.marketplace_product_id == marketplace_product_id
|
||||
)
|
||||
.first() is not None
|
||||
)
|
||||
|
||||
def _can_access_shop(self, shop: Shop, user: User) -> bool:
|
||||
"""Check if user can access shop."""
|
||||
# Admins and owners can always access
|
||||
if user.role == "admin" or shop.owner_id == user.id:
|
||||
return True
|
||||
|
||||
# Others can only access active and verified shops
|
||||
return shop.is_active and shop.is_verified
|
||||
|
||||
def _is_shop_owner(self, shop: Shop, user: User) -> bool:
|
||||
"""Check if user is shop owner."""
|
||||
return shop.owner_id == user.id
|
||||
|
||||
# Create service instance following the same pattern as other services
|
||||
shop_service = ShopService()
|
||||
@@ -44,7 +44,7 @@ class StatsService:
|
||||
unique_brands = self._get_unique_brands_count(db)
|
||||
unique_categories = self._get_unique_categories_count(db)
|
||||
unique_marketplaces = self._get_unique_marketplaces_count(db)
|
||||
unique_shops = self._get_unique_shops_count(db)
|
||||
unique_vendors = self._get_unique_vendors_count(db)
|
||||
|
||||
# Stock statistics
|
||||
stock_stats = self._get_stock_statistics(db)
|
||||
@@ -54,7 +54,7 @@ class StatsService:
|
||||
"unique_brands": unique_brands,
|
||||
"unique_categories": unique_categories,
|
||||
"unique_marketplaces": unique_marketplaces,
|
||||
"unique_shops": unique_shops,
|
||||
"unique_vendors": unique_vendors,
|
||||
"total_stock_entries": stock_stats["total_stock_entries"],
|
||||
"total_inventory_quantity": stock_stats["total_inventory_quantity"],
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class StatsService:
|
||||
db.query(
|
||||
MarketplaceProduct.marketplace,
|
||||
func.count(MarketplaceProduct.id).label("total_products"),
|
||||
func.count(func.distinct(MarketplaceProduct.shop_name)).label("unique_shops"),
|
||||
func.count(func.distinct(MarketplaceProduct.vendor_name)).label("unique_vendors"),
|
||||
func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"),
|
||||
)
|
||||
.filter(MarketplaceProduct.marketplace.isnot(None))
|
||||
@@ -99,7 +99,7 @@ class StatsService:
|
||||
{
|
||||
"marketplace": stat.marketplace,
|
||||
"total_products": stat.total_products,
|
||||
"unique_shops": stat.unique_shops,
|
||||
"unique_vendors": stat.unique_vendors,
|
||||
"unique_brands": stat.unique_brands,
|
||||
}
|
||||
for stat in marketplace_stats
|
||||
@@ -130,7 +130,7 @@ class StatsService:
|
||||
"unique_brands": self._get_unique_brands_count(db),
|
||||
"unique_categories": self._get_unique_categories_count(db),
|
||||
"unique_marketplaces": self._get_unique_marketplaces_count(db),
|
||||
"unique_shops": self._get_unique_shops_count(db),
|
||||
"unique_vendors": self._get_unique_vendors_count(db),
|
||||
"products_with_gtin": self._get_products_with_gtin_count(db),
|
||||
"products_with_images": self._get_products_with_images_count(db),
|
||||
}
|
||||
@@ -175,15 +175,15 @@ class StatsService:
|
||||
|
||||
product_count = self._get_products_by_marketplace_count(db, marketplace)
|
||||
brands = self._get_brands_by_marketplace(db, marketplace)
|
||||
shops = self._get_shops_by_marketplace(db, marketplace)
|
||||
vendors =self._get_vendors_by_marketplace(db, marketplace)
|
||||
|
||||
return {
|
||||
"marketplace": marketplace,
|
||||
"total_products": product_count,
|
||||
"unique_brands": len(brands),
|
||||
"unique_shops": len(shops),
|
||||
"unique_vendors": len(vendors),
|
||||
"brands": brands,
|
||||
"shops": shops,
|
||||
"vendors": vendors,
|
||||
}
|
||||
|
||||
except ValidationException:
|
||||
@@ -227,11 +227,11 @@ class StatsService:
|
||||
.count()
|
||||
)
|
||||
|
||||
def _get_unique_shops_count(self, db: Session) -> int:
|
||||
"""Get count of unique shops."""
|
||||
def _get_unique_vendors_count(self, db: Session) -> int:
|
||||
"""Get count of unique vendors."""
|
||||
return (
|
||||
db.query(MarketplaceProduct.shop_name)
|
||||
.filter(MarketplaceProduct.shop_name.isnot(None), MarketplaceProduct.shop_name != "")
|
||||
db.query(MarketplaceProduct.vendor_name)
|
||||
.filter(MarketplaceProduct.vendor_name.isnot(None), MarketplaceProduct.vendor_name != "")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
@@ -276,19 +276,19 @@ class StatsService:
|
||||
)
|
||||
return [brand[0] for brand in brands]
|
||||
|
||||
def _get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
|
||||
"""Get unique shops for a specific marketplace."""
|
||||
shops = (
|
||||
db.query(MarketplaceProduct.shop_name)
|
||||
def _get_vendors_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
|
||||
"""Get unique vendors for a specific marketplace."""
|
||||
vendors =(
|
||||
db.query(MarketplaceProduct.vendor_name)
|
||||
.filter(
|
||||
MarketplaceProduct.marketplace == marketplace,
|
||||
MarketplaceProduct.shop_name.isnot(None),
|
||||
MarketplaceProduct.shop_name != "",
|
||||
MarketplaceProduct.vendor_name.isnot(None),
|
||||
MarketplaceProduct.vendor_name != "",
|
||||
)
|
||||
.distinct()
|
||||
.all()
|
||||
)
|
||||
return [shop[0] for shop in shops]
|
||||
return [vendor [0] for vendor in vendors]
|
||||
|
||||
def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int:
|
||||
"""Get product count for a specific marketplace."""
|
||||
|
||||
359
app/services/vendor_service.py
Normal file
359
app/services/vendor_service.py
Normal file
@@ -0,0 +1,359 @@
|
||||
# app/services/vendor_service.py
|
||||
"""
|
||||
Vendor service for managing vendor operations and product catalog.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Vendor creation and management
|
||||
- Vendor access control and validation
|
||||
- Vendor product catalog operations
|
||||
- Vendor filtering and search
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
VendorNotFoundException,
|
||||
VendorAlreadyExistsException,
|
||||
UnauthorizedVendorAccessException,
|
||||
InvalidVendorDataException,
|
||||
MarketplaceProductNotFoundException,
|
||||
ProductAlreadyExistsException,
|
||||
MaxVendorsReachedException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.vendor import VendorCreate
|
||||
from models.schemas.product import ProductCreate
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.product import Product
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VendorService:
|
||||
"""Service class for vendor operations following the application's service pattern."""
|
||||
|
||||
def create_vendor(
|
||||
self, db: Session, vendor_data: VendorCreate, current_user: User
|
||||
) -> Vendor:
|
||||
"""
|
||||
Create a new vendor.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_data: Vendor creation data
|
||||
current_user: User creating the vendor
|
||||
|
||||
Returns:
|
||||
Created vendor object
|
||||
|
||||
Raises:
|
||||
VendorAlreadyExistsException: If vendor code already exists
|
||||
MaxVendorsReachedException: If user has reached maximum vendors
|
||||
InvalidVendorDataException: If vendor data is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate vendor data
|
||||
self._validate_vendor_data(vendor_data)
|
||||
|
||||
# Check user's vendor limit (if applicable)
|
||||
self._check_vendor_limit(db, current_user)
|
||||
|
||||
# Normalize vendor code to uppercase
|
||||
normalized_vendor_code = vendor_data.vendor_code.upper()
|
||||
|
||||
# Check if vendor code already exists (case-insensitive check)
|
||||
if self._vendor_code_exists(db, normalized_vendor_code):
|
||||
raise VendorAlreadyExistsException(normalized_vendor_code)
|
||||
|
||||
# Create vendor with uppercase code
|
||||
vendor_dict = vendor_data.model_dump()
|
||||
vendor_dict["vendor_code"] = normalized_vendor_code # Store as uppercase
|
||||
|
||||
new_vendor = Vendor(
|
||||
**vendor_dict,
|
||||
owner_id=current_user.id,
|
||||
is_active=True,
|
||||
is_verified=(current_user.role == "admin"),
|
||||
)
|
||||
|
||||
db.add(new_vendor)
|
||||
db.commit()
|
||||
db.refresh(new_vendor)
|
||||
|
||||
logger.info(
|
||||
f"New vendor created: {new_vendor.vendor_code} by {current_user.username}"
|
||||
)
|
||||
return new_vendor
|
||||
|
||||
except (VendorAlreadyExistsException, MaxVendorsReachedException, InvalidVendorDataException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating vendor : {str(e)}")
|
||||
raise ValidationException("Failed to create vendor ")
|
||||
|
||||
def get_vendors(
|
||||
self,
|
||||
db: Session,
|
||||
current_user: User,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
verified_only: bool = False,
|
||||
) -> Tuple[List[Vendor], int]:
|
||||
"""
|
||||
Get vendors with filtering.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
current_user: Current user requesting vendors
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
active_only: Filter for active vendors only
|
||||
verified_only: Filter for verified vendors only
|
||||
|
||||
Returns:
|
||||
Tuple of (vendors_list, total_count)
|
||||
"""
|
||||
try:
|
||||
query = db.query(Vendor)
|
||||
|
||||
# Non-admin users can only see active and verified vendors, plus their own
|
||||
if current_user.role != "admin":
|
||||
query = query.filter(
|
||||
(Vendor.is_active == True)
|
||||
& ((Vendor.is_verified == True) | (Vendor.owner_id == current_user.id))
|
||||
)
|
||||
else:
|
||||
# Admin can apply filters
|
||||
if active_only:
|
||||
query = query.filter(Vendor.is_active == True)
|
||||
if verified_only:
|
||||
query = query.filter(Vendor.is_verified == True)
|
||||
|
||||
total = query.count()
|
||||
vendors = query.offset(skip).limit(limit).all()
|
||||
|
||||
return vendors, total
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting vendors: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve vendors")
|
||||
|
||||
def get_vendor_by_code(self, db: Session, vendor_code: str, current_user: User) -> Vendor:
|
||||
"""
|
||||
Get vendor by vendor code with access control.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_code: Vendor code to find
|
||||
current_user: Current user requesting the vendor
|
||||
|
||||
Returns:
|
||||
Vendor object
|
||||
|
||||
Raises:
|
||||
VendorNotFoundException: If vendor not found
|
||||
UnauthorizedVendorAccessException: If access denied
|
||||
"""
|
||||
try:
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not vendor :
|
||||
raise VendorNotFoundException(vendor_code)
|
||||
|
||||
# Check access permissions
|
||||
if not self._can_access_vendor(vendor, current_user):
|
||||
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
|
||||
|
||||
return vendor
|
||||
|
||||
except (VendorNotFoundException, UnauthorizedVendorAccessException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting vendor {vendor_code}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve vendor ")
|
||||
|
||||
def add_product_to_catalog(
|
||||
self, db: Session, vendor : Vendor, product: ProductCreate
|
||||
) -> Product:
|
||||
"""
|
||||
Add existing product to vendor catalog with vendor -specific settings.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor : Vendor to add product to
|
||||
product: Vendor product data
|
||||
|
||||
Returns:
|
||||
Created Product object
|
||||
|
||||
Raises:
|
||||
MarketplaceProductNotFoundException: If product not found
|
||||
ProductAlreadyExistsException: If product already in vendor
|
||||
"""
|
||||
try:
|
||||
# Check if product exists
|
||||
marketplace_product = self._get_product_by_id_or_raise(db, product.marketplace_product_id)
|
||||
|
||||
# Check if product already in vendor
|
||||
if self._product_in_catalog(db, vendor.id, marketplace_product.id):
|
||||
raise ProductAlreadyExistsException(vendor.vendor_code, product.marketplace_product_id)
|
||||
|
||||
# Create vendor -product association
|
||||
new_product = Product(
|
||||
vendor_id=vendor.id,
|
||||
marketplace_product_id=marketplace_product.id,
|
||||
**product.model_dump(exclude={"marketplace_product_id"}),
|
||||
)
|
||||
|
||||
db.add(new_product)
|
||||
db.commit()
|
||||
db.refresh(new_product)
|
||||
|
||||
# Load the product relationship
|
||||
db.refresh(new_product)
|
||||
|
||||
logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to vendor {vendor.vendor_code}")
|
||||
return new_product
|
||||
|
||||
except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
|
||||
db.rollback()
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error adding product to vendor : {str(e)}")
|
||||
raise ValidationException("Failed to add product to vendor ")
|
||||
|
||||
def get_products(
|
||||
self,
|
||||
db: Session,
|
||||
vendor : Vendor,
|
||||
current_user: User,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
active_only: bool = True,
|
||||
featured_only: bool = False,
|
||||
) -> Tuple[List[Product], int]:
|
||||
"""
|
||||
Get products in vendor catalog with filtering.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor : Vendor to get products from
|
||||
current_user: Current user requesting products
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
active_only: Filter for active products only
|
||||
featured_only: Filter for featured products only
|
||||
|
||||
Returns:
|
||||
Tuple of (products_list, total_count)
|
||||
|
||||
Raises:
|
||||
UnauthorizedVendorAccessException: If vendor access denied
|
||||
"""
|
||||
try:
|
||||
# Check access permissions
|
||||
if not self._can_access_vendor(vendor, current_user):
|
||||
raise UnauthorizedVendorAccessException(vendor.vendor_code, current_user.id)
|
||||
|
||||
# Query vendor products
|
||||
query = db.query(Product).filter(Product.vendor_id == vendor.id)
|
||||
|
||||
if active_only:
|
||||
query = query.filter(Product.is_active == True)
|
||||
if featured_only:
|
||||
query = query.filter(Product.is_featured == True)
|
||||
|
||||
total = query.count()
|
||||
products = query.offset(skip).limit(limit).all()
|
||||
|
||||
return products, total
|
||||
|
||||
except UnauthorizedVendorAccessException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting vendor products: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve vendor products")
|
||||
|
||||
# Private helper methods
|
||||
def _validate_vendor_data(self, vendor_data: VendorCreate) -> None:
|
||||
"""Validate vendor creation data."""
|
||||
if not vendor_data.vendor_code or not vendor_data.vendor_code.strip():
|
||||
raise InvalidVendorDataException("Vendor code is required", field="vendor_code")
|
||||
|
||||
if not vendor_data.vendor_name or not vendor_data.vendor_name.strip():
|
||||
raise InvalidVendorDataException("Vendor name is required", field="vendor_name")
|
||||
|
||||
# Validate vendor code format (alphanumeric, underscores, hyphens)
|
||||
import re
|
||||
if not re.match(r'^[A-Za-z0-9_-]+$', vendor_data.vendor_code):
|
||||
raise InvalidVendorDataException(
|
||||
"Vendor code can only contain letters, numbers, underscores, and hyphens",
|
||||
field="vendor_code"
|
||||
)
|
||||
|
||||
def _check_vendor_limit(self, db: Session, user: User) -> None:
|
||||
"""Check if user has reached maximum vendor limit."""
|
||||
if user.role == "admin":
|
||||
return # Admins have no limit
|
||||
|
||||
user_vendor_count = db.query(Vendor).filter(Vendor.owner_id == user.id).count()
|
||||
max_vendors = 5 # Configure this as needed
|
||||
|
||||
if user_vendor_count >= max_vendors:
|
||||
raise MaxVendorsReachedException(max_vendors, user.id)
|
||||
|
||||
def _vendor_code_exists(self, db: Session, vendor_code: str) -> bool:
|
||||
"""Check if vendor code already exists (case-insensitive)."""
|
||||
return (
|
||||
db.query(Vendor)
|
||||
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
|
||||
.first() is not None
|
||||
)
|
||||
|
||||
def _get_product_by_id_or_raise(self, db: Session, marketplace_product_id: str) -> MarketplaceProduct:
|
||||
"""Get product by ID or raise exception."""
|
||||
product = db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
|
||||
if not product:
|
||||
raise MarketplaceProductNotFoundException(marketplace_product_id)
|
||||
return product
|
||||
|
||||
def _product_in_catalog(self, db: Session, vendor_id: int, marketplace_product_id: int) -> bool:
|
||||
"""Check if product is already in vendor."""
|
||||
return (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.marketplace_product_id == marketplace_product_id
|
||||
)
|
||||
.first() is not None
|
||||
)
|
||||
|
||||
def _can_access_vendor(self, vendor : Vendor, user: User) -> bool:
|
||||
"""Check if user can access vendor."""
|
||||
# Admins and owners can always access
|
||||
if user.role == "admin" or vendor.owner_id == user.id:
|
||||
return True
|
||||
|
||||
# Others can only access active and verified vendors
|
||||
return vendor.is_active and vendor.is_verified
|
||||
|
||||
def _is_vendor_owner(self, vendor : Vendor, user: User) -> bool:
|
||||
"""Check if user is vendor owner."""
|
||||
return vendor.owner_id == user.id
|
||||
|
||||
# Create service instance following the same pattern as other services
|
||||
vendor_service = VendorService()
|
||||
Reference in New Issue
Block a user