Exception handling enhancement
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
# app/services/marketplace_service.py
|
||||
"""Summary description ....
|
||||
"""
|
||||
Marketplace service for managing import jobs and marketplace integrations.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- ....
|
||||
- ....
|
||||
- ....
|
||||
- Import job creation and management
|
||||
- Shop access validation
|
||||
- Import job status tracking and updates
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -14,6 +15,15 @@ from typing import List, Optional
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
ShopNotFoundException,
|
||||
UnauthorizedShopAccessException,
|
||||
ImportJobNotFoundException,
|
||||
ImportJobNotOwnedException,
|
||||
ImportJobCannotBeCancelledException,
|
||||
ImportJobCannotBeDeletedException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.marketplace import (MarketplaceImportJobResponse,
|
||||
MarketplaceImportRequest)
|
||||
from models.database.marketplace import MarketplaceImportJob
|
||||
@@ -26,171 +36,283 @@ logger = logging.getLogger(__name__)
|
||||
class MarketplaceService:
|
||||
"""Service class for Marketplace operations following the application's service pattern."""
|
||||
|
||||
def __init__(self):
|
||||
"""Class constructor."""
|
||||
pass
|
||||
|
||||
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
|
||||
"""Validate that the shop exists and user has access to it."""
|
||||
# Explicit type hint to help type checker shop: Optional[Shop]
|
||||
# Use case-insensitive query to handle both uppercase and lowercase codes
|
||||
shop: Optional[Shop] = (
|
||||
db.query(Shop)
|
||||
.filter(func.upper(Shop.shop_code) == shop_code.upper())
|
||||
.first()
|
||||
)
|
||||
if not shop:
|
||||
raise ValueError("Shop not found")
|
||||
"""
|
||||
Validate that the shop exists and user has access to it.
|
||||
|
||||
# Check permissions: admin can import for any shop, others only for their own
|
||||
if user.role != "admin" and shop.owner_id != user.id:
|
||||
raise PermissionError("Access denied to this shop")
|
||||
Args:
|
||||
db: Database session
|
||||
shop_code: Shop code to validate
|
||||
user: User requesting access
|
||||
|
||||
return shop
|
||||
Returns:
|
||||
Shop object if access is valid
|
||||
|
||||
Raises:
|
||||
ShopNotFoundException: If shop doesn't exist
|
||||
UnauthorizedShopAccessException: 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())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not shop:
|
||||
raise ShopNotFoundException(shop_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)
|
||||
|
||||
return shop
|
||||
|
||||
except (ShopNotFoundException, UnauthorizedShopAccessException):
|
||||
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")
|
||||
|
||||
def create_import_job(
|
||||
self, db: Session, request: MarketplaceImportRequest, user: User
|
||||
self, db: Session, request: MarketplaceImportRequest, user: User
|
||||
) -> MarketplaceImportJob:
|
||||
"""Create a new marketplace import job."""
|
||||
# Validate shop access first
|
||||
shop = self.validate_shop_access(db, request.shop_code, user)
|
||||
"""
|
||||
Create a new marketplace import job.
|
||||
|
||||
# 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)
|
||||
user_id=user.id,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
Args:
|
||||
db: Database session
|
||||
request: Import request data
|
||||
user: User creating the job
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
Returns:
|
||||
Created MarketplaceImportJob object
|
||||
|
||||
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}"
|
||||
)
|
||||
Raises:
|
||||
ShopNotFoundException: If shop doesn't exist
|
||||
UnauthorizedShopAccessException: If user lacks shop access
|
||||
ValidationException: If job creation fails
|
||||
"""
|
||||
try:
|
||||
# Validate shop access first
|
||||
shop = self.validate_shop_access(db, request.shop_code, user)
|
||||
|
||||
return import_job
|
||||
# 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)
|
||||
user_id=user.id,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
return import_job
|
||||
|
||||
except (ShopNotFoundException, UnauthorizedShopAccessException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating import job: {str(e)}")
|
||||
raise ValidationException("Failed to create import job")
|
||||
|
||||
def get_import_job_by_id(
|
||||
self, db: Session, job_id: int, user: User
|
||||
self, db: Session, job_id: int, user: User
|
||||
) -> MarketplaceImportJob:
|
||||
"""Get a marketplace import job by ID with access control."""
|
||||
job = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
if not job:
|
||||
raise ValueError("Marketplace import job not found")
|
||||
"""
|
||||
Get a marketplace import job by ID with access control.
|
||||
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin" and job.user_id != user.id:
|
||||
raise PermissionError("Access denied to this import job")
|
||||
Args:
|
||||
db: Database session
|
||||
job_id: Import job ID
|
||||
user: User requesting the job
|
||||
|
||||
return job
|
||||
Returns:
|
||||
MarketplaceImportJob object
|
||||
|
||||
Raises:
|
||||
ImportJobNotFoundException: If job doesn't exist
|
||||
ImportJobNotOwnedException: If user lacks access to job
|
||||
"""
|
||||
try:
|
||||
job = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not job:
|
||||
raise ImportJobNotFoundException(job_id)
|
||||
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin" and job.user_id != user.id:
|
||||
raise ImportJobNotOwnedException(job_id, user.id)
|
||||
|
||||
return job
|
||||
|
||||
except (ImportJobNotFoundException, ImportJobNotOwnedException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting import job {job_id}: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve import job")
|
||||
|
||||
def get_import_jobs(
|
||||
self,
|
||||
db: Session,
|
||||
user: User,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
self,
|
||||
db: Session,
|
||||
user: User,
|
||||
marketplace: Optional[str] = None,
|
||||
shop_name: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[MarketplaceImportJob]:
|
||||
"""Get marketplace import jobs with filtering and access control."""
|
||||
query = db.query(MarketplaceImportJob)
|
||||
"""
|
||||
Get marketplace import jobs with filtering and access control.
|
||||
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin":
|
||||
query = query.filter(MarketplaceImportJob.user_id == user.id)
|
||||
Args:
|
||||
db: Database session
|
||||
user: User requesting jobs
|
||||
marketplace: Optional marketplace filter
|
||||
shop_name: Optional shop name filter
|
||||
skip: Number of records to skip
|
||||
limit: Maximum records to return
|
||||
|
||||
# Apply filters
|
||||
if marketplace:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
Returns:
|
||||
List of MarketplaceImportJob objects
|
||||
"""
|
||||
try:
|
||||
query = db.query(MarketplaceImportJob)
|
||||
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin":
|
||||
query = query.filter(MarketplaceImportJob.user_id == user.id)
|
||||
|
||||
# Apply filters
|
||||
if marketplace:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
|
||||
|
||||
# Order by creation date (newest first) and apply pagination
|
||||
jobs = (
|
||||
query.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
if shop_name:
|
||||
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
|
||||
|
||||
# Order by creation date (newest first) and apply pagination
|
||||
jobs = (
|
||||
query.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return jobs
|
||||
|
||||
return jobs
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting import jobs: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve import jobs")
|
||||
|
||||
def update_job_status(
|
||||
self, db: Session, job_id: int, status: str, **kwargs
|
||||
self, db: Session, job_id: int, status: str, **kwargs
|
||||
) -> MarketplaceImportJob:
|
||||
"""Update marketplace import job status and other fields."""
|
||||
job = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
if not job:
|
||||
raise ValueError("Marketplace import job not found")
|
||||
"""
|
||||
Update marketplace import job status and other fields.
|
||||
|
||||
job.status = status
|
||||
Args:
|
||||
db: Database session
|
||||
job_id: Import job ID
|
||||
status: New status
|
||||
**kwargs: Additional fields to update
|
||||
|
||||
# Update optional fields if provided
|
||||
if "imported_count" in kwargs:
|
||||
job.imported_count = kwargs["imported_count"]
|
||||
if "updated_count" in kwargs:
|
||||
job.updated_count = kwargs["updated_count"]
|
||||
if "total_processed" in kwargs:
|
||||
job.total_processed = kwargs["total_processed"]
|
||||
if "error_count" in kwargs:
|
||||
job.error_count = kwargs["error_count"]
|
||||
if "error_message" in kwargs:
|
||||
job.error_message = kwargs["error_message"]
|
||||
if "started_at" in kwargs:
|
||||
job.started_at = kwargs["started_at"]
|
||||
if "completed_at" in kwargs:
|
||||
job.completed_at = kwargs["completed_at"]
|
||||
Returns:
|
||||
Updated MarketplaceImportJob object
|
||||
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
Raises:
|
||||
ImportJobNotFoundException: If job doesn't exist
|
||||
ValidationException: If update fails
|
||||
"""
|
||||
try:
|
||||
job = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
logger.info(f"Updated marketplace import job {job_id} status to {status}")
|
||||
return job
|
||||
if not job:
|
||||
raise ImportJobNotFoundException(job_id)
|
||||
|
||||
job.status = status
|
||||
|
||||
# Update optional fields if provided
|
||||
allowed_fields = [
|
||||
'imported_count', 'updated_count', 'total_processed',
|
||||
'error_count', 'error_message', 'started_at', 'completed_at'
|
||||
]
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in kwargs:
|
||||
setattr(job, field, kwargs[field])
|
||||
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
logger.info(f"Updated marketplace import job {job_id} status to {status}")
|
||||
return job
|
||||
|
||||
except ImportJobNotFoundException:
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating job {job_id} status: {str(e)}")
|
||||
raise ValidationException("Failed to update job status")
|
||||
|
||||
def get_job_stats(self, db: Session, user: User) -> dict:
|
||||
"""Get statistics about marketplace import jobs for a user."""
|
||||
query = db.query(MarketplaceImportJob)
|
||||
"""
|
||||
Get statistics about marketplace import jobs for a user.
|
||||
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin":
|
||||
query = query.filter(MarketplaceImportJob.user_id == user.id)
|
||||
Args:
|
||||
db: Database session
|
||||
user: User to get stats for
|
||||
|
||||
total_jobs = query.count()
|
||||
pending_jobs = query.filter(MarketplaceImportJob.status == "pending").count()
|
||||
running_jobs = query.filter(MarketplaceImportJob.status == "running").count()
|
||||
completed_jobs = query.filter(
|
||||
MarketplaceImportJob.status == "completed"
|
||||
).count()
|
||||
failed_jobs = query.filter(MarketplaceImportJob.status == "failed").count()
|
||||
Returns:
|
||||
Dictionary containing job statistics
|
||||
"""
|
||||
try:
|
||||
query = db.query(MarketplaceImportJob)
|
||||
|
||||
return {
|
||||
"total_jobs": total_jobs,
|
||||
"pending_jobs": pending_jobs,
|
||||
"running_jobs": running_jobs,
|
||||
"completed_jobs": completed_jobs,
|
||||
"failed_jobs": failed_jobs,
|
||||
}
|
||||
# Users can only see their own jobs, admins can see all
|
||||
if user.role != "admin":
|
||||
query = query.filter(MarketplaceImportJob.user_id == user.id)
|
||||
|
||||
total_jobs = query.count()
|
||||
pending_jobs = query.filter(MarketplaceImportJob.status == "pending").count()
|
||||
running_jobs = query.filter(MarketplaceImportJob.status == "running").count()
|
||||
completed_jobs = query.filter(
|
||||
MarketplaceImportJob.status == "completed"
|
||||
).count()
|
||||
failed_jobs = query.filter(MarketplaceImportJob.status == "failed").count()
|
||||
|
||||
return {
|
||||
"total_jobs": total_jobs,
|
||||
"pending_jobs": pending_jobs,
|
||||
"running_jobs": running_jobs,
|
||||
"completed_jobs": completed_jobs,
|
||||
"failed_jobs": failed_jobs,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting job stats: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve job statistics")
|
||||
|
||||
def convert_to_response_model(
|
||||
self, job: MarketplaceImportJob
|
||||
self, job: MarketplaceImportJob
|
||||
) -> MarketplaceImportJobResponse:
|
||||
"""Convert database model to API response model."""
|
||||
return MarketplaceImportJobResponse(
|
||||
@@ -213,38 +335,82 @@ class MarketplaceService:
|
||||
)
|
||||
|
||||
def cancel_import_job(
|
||||
self, db: Session, job_id: int, user: User
|
||||
self, db: Session, job_id: int, user: User
|
||||
) -> MarketplaceImportJob:
|
||||
"""Cancel a pending or running import job."""
|
||||
job = self.get_import_job_by_id(db, job_id, user)
|
||||
"""
|
||||
Cancel a pending or running import job.
|
||||
|
||||
if job.status not in ["pending", "running"]:
|
||||
raise ValueError(f"Cannot cancel job with status: {job.status}")
|
||||
Args:
|
||||
db: Database session
|
||||
job_id: Import job ID
|
||||
user: User requesting cancellation
|
||||
|
||||
job.status = "cancelled"
|
||||
job.completed_at = datetime.utcnow()
|
||||
Returns:
|
||||
Updated MarketplaceImportJob object
|
||||
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
Raises:
|
||||
ImportJobNotFoundException: If job doesn't exist
|
||||
ImportJobNotOwnedException: If user lacks access
|
||||
ImportJobCannotBeCancelledException: If job can't be cancelled
|
||||
"""
|
||||
try:
|
||||
job = self.get_import_job_by_id(db, job_id, user)
|
||||
|
||||
logger.info(f"Cancelled marketplace import job {job_id}")
|
||||
return job
|
||||
if job.status not in ["pending", "running"]:
|
||||
raise ImportJobCannotBeCancelledException(job_id, job.status)
|
||||
|
||||
job.status = "cancelled"
|
||||
job.completed_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
logger.info(f"Cancelled marketplace import job {job_id}")
|
||||
return job
|
||||
|
||||
except (ImportJobNotFoundException, ImportJobNotOwnedException, ImportJobCannotBeCancelledException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error cancelling job {job_id}: {str(e)}")
|
||||
raise ValidationException("Failed to cancel import job")
|
||||
|
||||
def delete_import_job(self, db: Session, job_id: int, user: User) -> bool:
|
||||
"""Delete a marketplace import job."""
|
||||
job = self.get_import_job_by_id(db, job_id, user)
|
||||
"""
|
||||
Delete a marketplace import job.
|
||||
|
||||
# Only allow deletion of completed, failed, or cancelled jobs
|
||||
if job.status in ["pending", "running"]:
|
||||
raise ValueError(
|
||||
f"Cannot delete job with status: {job.status}. Cancel it first."
|
||||
)
|
||||
Args:
|
||||
db: Database session
|
||||
job_id: Import job ID
|
||||
user: User requesting deletion
|
||||
|
||||
db.delete(job)
|
||||
db.commit()
|
||||
Returns:
|
||||
True if deletion successful
|
||||
|
||||
logger.info(f"Deleted marketplace import job {job_id}")
|
||||
return True
|
||||
Raises:
|
||||
ImportJobNotFoundException: If job doesn't exist
|
||||
ImportJobNotOwnedException: If user lacks access
|
||||
ImportJobCannotBeDeletedException: If job can't be deleted
|
||||
"""
|
||||
try:
|
||||
job = self.get_import_job_by_id(db, job_id, user)
|
||||
|
||||
# Only allow deletion of completed, failed, or cancelled jobs
|
||||
if job.status in ["pending", "running"]:
|
||||
raise ImportJobCannotBeDeletedException(job_id, job.status)
|
||||
|
||||
db.delete(job)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted marketplace import job {job_id}")
|
||||
return True
|
||||
|
||||
except (ImportJobNotFoundException, ImportJobNotOwnedException, ImportJobCannotBeDeletedException):
|
||||
raise # Re-raise custom exceptions
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting job {job_id}: {str(e)}")
|
||||
raise ValidationException("Failed to delete import job")
|
||||
|
||||
|
||||
# Create service instance
|
||||
|
||||
Reference in New Issue
Block a user