from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from models.database_models import Product, Stock from models.api_models import ProductCreate, ProductUpdate, StockLocationResponse, StockSummaryResponse from utils.data_processing import GTINProcessor, PriceProcessor from typing import Optional, List, Generator from datetime import datetime import logging logger = logging.getLogger(__name__) class ProductService: def __init__(self): 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()