# app/services/marketplace_service.py """Summary description .... This module provides classes and functions for: - .... - .... - .... """ import logging from datetime import datetime from typing import List, Optional from sqlalchemy import func from sqlalchemy.orm import Session from models.api.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 __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") # 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") return shop def create_import_job( 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 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 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.""" job = ( db.query(MarketplaceImportJob) .filter(MarketplaceImportJob.id == job_id) .first() ) if not job: raise ValueError("Marketplace import job not found") # 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") return 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.""" 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 def update_job_status( 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") job.status = status # 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"] db.commit() db.refresh(job) logger.info(f"Updated marketplace import job {job_id} status to {status}") return job def get_job_stats(self, db: Session, user: User) -> dict: """Get statistics about marketplace import jobs for a user.""" 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, } 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.""" job = self.get_import_job_by_id(db, job_id, user) if job.status not in ["pending", "running"]: raise ValueError(f"Cannot cancel job with status: {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 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) # 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." ) db.delete(job) db.commit() logger.info(f"Deleted marketplace import job {job_id}") return True # Create service instance marketplace_service = MarketplaceService()