Files
orion/app/services/marketplace_service.py

418 lines
14 KiB
Python

# app/services/marketplace_service.py
"""
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
from datetime import datetime, timezone
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
from models.database.shop import Shop
from models.database.user import User
logger = logging.getLogger(__name__)
class MarketplaceService:
"""Service class for Marketplace operations following the application's service pattern."""
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
"""
Validate that the shop exists and user has access to it.
Args:
db: Database session
shop_code: Shop code to validate
user: User requesting access
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
) -> MarketplaceImportJob:
"""
Create a new marketplace import job.
Args:
db: Database session
request: Import request data
user: User creating the job
Returns:
Created MarketplaceImportJob object
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)
# 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.now(timezone.utc),
)
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
) -> MarketplaceImportJob:
"""
Get a marketplace import job by ID with access control.
Args:
db: Database session
job_id: Import job ID
user: User requesting the 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,
) -> List[MarketplaceImportJob]:
"""
Get marketplace import jobs with filtering and access control.
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
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()
)
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
) -> MarketplaceImportJob:
"""
Update marketplace import job status and other fields.
Args:
db: Database session
job_id: Import job ID
status: New status
**kwargs: Additional fields to update
Returns:
Updated MarketplaceImportJob object
Raises:
ImportJobNotFoundException: If job doesn't exist
ValidationException: If update fails
"""
try:
job = (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.id == job_id)
.first()
)
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.
Args:
db: Database session
user: User to get stats for
Returns:
Dictionary containing job statistics
"""
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)
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
) -> MarketplaceImportJobResponse:
"""Convert database model to API response model."""
return MarketplaceImportJobResponse(
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
), # Add this optional field via relationship
shop_name=job.shop_name,
imported=job.imported_count or 0,
updated=job.updated_count or 0,
total_processed=job.total_processed or 0,
error_count=job.error_count or 0,
error_message=job.error_message,
created_at=job.created_at,
started_at=job.started_at,
completed_at=job.completed_at,
)
def cancel_import_job(
self, db: Session, job_id: int, user: User
) -> MarketplaceImportJob:
"""
Cancel a pending or running import job.
Args:
db: Database session
job_id: Import job ID
user: User requesting cancellation
Returns:
Updated MarketplaceImportJob object
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)
if job.status not in ["pending", "running"]:
raise ImportJobCannotBeCancelledException(job_id, job.status)
job.status = "cancelled"
job.completed_at = datetime.now(timezone.utc)
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.
Args:
db: Database session
job_id: Import job ID
user: User requesting deletion
Returns:
True if deletion successful
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
marketplace_service = MarketplaceService()