# app/services/product_service.py """Summary description .... This module provides classes and functions for: - .... - .... - .... """ import logging from datetime import datetime from typing import Generator, List, Optional from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from models.schemas.product import ProductCreate, ProductUpdate from models.schemas.stock import StockLocationResponse, StockSummaryResponse from models.database.product import Product from models.database.stock import Stock from app.utils.data_processing import GTINProcessor, PriceProcessor logger = logging.getLogger(__name__) class ProductService: """Service class for Product operations following the application's service pattern.""" def __init__(self): """Class constructor.""" self.gtin_processor = GTINProcessor() self.price_processor = PriceProcessor() def create_product(self, db: Session, product_data: ProductCreate) -> Product: """Create a new product with validation.""" try: # Process and validate GTIN if provided if product_data.gtin: normalized_gtin = self.gtin_processor.normalize(product_data.gtin) if not normalized_gtin: raise ValueError("Invalid GTIN format") product_data.gtin = normalized_gtin # Process price if provided if product_data.price: parsed_price, currency = self.price_processor.parse_price_currency( product_data.price ) if parsed_price: product_data.price = parsed_price product_data.currency = currency # Set default marketplace if not provided if not product_data.marketplace: product_data.marketplace = "Letzshop" db_product = Product(**product_data.model_dump()) db.add(db_product) db.commit() db.refresh(db_product) logger.info(f"Created product {db_product.product_id}") return db_product except IntegrityError as e: db.rollback() logger.error(f"Database integrity error: {str(e)}") raise ValueError("Product with this ID already exists") except Exception as e: db.rollback() logger.error(f"Error creating product: {str(e)}") raise def get_product_by_id(self, db: Session, product_id: str) -> Optional[Product]: """Get a product by its ID.""" return db.query(Product).filter(Product.product_id == product_id).first() def get_products_with_filters( self, db: Session, skip: int = 0, limit: int = 100, brand: Optional[str] = None, category: Optional[str] = None, availability: Optional[str] = None, marketplace: Optional[str] = None, shop_name: Optional[str] = None, search: Optional[str] = None, ) -> tuple[List[Product], int]: """Get products with filtering and pagination.""" query = db.query(Product) # Apply filters if brand: query = query.filter(Product.brand.ilike(f"%{brand}%")) if category: query = query.filter(Product.google_product_category.ilike(f"%{category}%")) if availability: query = query.filter(Product.availability == availability) if marketplace: query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) if shop_name: query = query.filter(Product.shop_name.ilike(f"%{shop_name}%")) if search: # Search in title, description, marketplace, and shop_name search_term = f"%{search}%" query = query.filter( (Product.title.ilike(search_term)) | (Product.description.ilike(search_term)) | (Product.marketplace.ilike(search_term)) | (Product.shop_name.ilike(search_term)) ) total = query.count() products = query.offset(skip).limit(limit).all() return products, total def update_product( self, db: Session, product_id: str, product_update: ProductUpdate ) -> Product: """Update product with validation.""" product = db.query(Product).filter(Product.product_id == product_id).first() if not product: raise ValueError("Product not found") # Update fields update_data = product_update.model_dump(exclude_unset=True) # Validate GTIN if being updated if "gtin" in update_data and update_data["gtin"]: normalized_gtin = self.gtin_processor.normalize(update_data["gtin"]) if not normalized_gtin: raise ValueError("Invalid GTIN format") update_data["gtin"] = normalized_gtin # Process price if being updated if "price" in update_data and update_data["price"]: parsed_price, currency = self.price_processor.parse_price_currency( update_data["price"] ) if parsed_price: update_data["price"] = parsed_price update_data["currency"] = currency for key, value in update_data.items(): setattr(product, key, value) product.updated_at = datetime.utcnow() db.commit() db.refresh(product) logger.info(f"Updated product {product_id}") return product def delete_product(self, db: Session, product_id: str) -> bool: """Delete product and associated stock.""" product = db.query(Product).filter(Product.product_id == product_id).first() if not product: raise ValueError("Product not found") # Delete associated stock entries if GTIN exists if product.gtin: db.query(Stock).filter(Stock.gtin == product.gtin).delete() db.delete(product) db.commit() logger.info(f"Deleted product {product_id}") return True def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]: """Get stock information for a product by GTIN.""" stock_entries = db.query(Stock).filter(Stock.gtin == gtin).all() if not stock_entries: return None total_quantity = sum(entry.quantity for entry in stock_entries) locations = [ StockLocationResponse(location=entry.location, quantity=entry.quantity) for entry in stock_entries ] return StockSummaryResponse( gtin=gtin, total_quantity=total_quantity, locations=locations ) def generate_csv_export( self, db: Session, marketplace: Optional[str] = None, shop_name: Optional[str] = None, ) -> Generator[str, None, None]: """Generate CSV export with streaming for memory efficiency.""" # CSV header yield ( "product_id,title,description,link,image_link,availability,price,currency,brand," "gtin,marketplace,shop_name\n" ) batch_size = 1000 offset = 0 while True: query = db.query(Product) # Apply marketplace filters if marketplace: query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) if shop_name: query = query.filter(Product.shop_name.ilike(f"%{shop_name}%")) products = query.offset(offset).limit(batch_size).all() if not products: break for product in products: # Create CSV row with marketplace fields row = ( f'"{product.product_id}","{product.title or ""}","{product.description or ""}",' f'"{product.link or ""}","{product.image_link or ""}","{product.availability or ""}",' f'"{product.price or ""}","{product.currency or ""}","{product.brand or ""}",' f'"{product.gtin or ""}","{product.marketplace or ""}","{product.shop_name or ""}"\n' ) yield row offset += batch_size def product_exists(self, db: Session, product_id: str) -> bool: """Check if product exists by ID.""" return ( db.query(Product).filter(Product.product_id == product_id).first() is not None ) # Create service instance product_service = ProductService()