Files
orion/app/services/product_service.py

239 lines
8.4 KiB
Python

# 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.api.product import ProductCreate, ProductUpdate
from models.api.stock import StockLocationResponse, StockSummaryResponse
from models.database.product import Product
from models.database.stock import Stock
from 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()