from typing import List, Dict, Any, Tuple from sqlalchemy.orm import Session from app.models.ProductModel import Product, ProductVariant from app.models.MigrationModel import MediaGroup class ValidationEngine: """ Validates mapped data rows in Dry Run or pre-import validation pass. Checks: - Required fields (SKU, Name/Title) - Valid data types (Price numeric, Stock integer) - SKU collision & existing DB state for Import Mode evaluation - Media Key existence check in media_groups """ @classmethod def validate_row( cls, row_number: int, mapped_row: Dict[str, Any], db_session: Session, import_mode: str = "UPSERT", existing_sku_set: set = None, existing_media_key_set: set = None ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: """ Returns (errors, warnings) lists for a single row. """ errors = [] warnings = [] sku = mapped_row.get("sku") name = mapped_row.get("name") price = mapped_row.get("price") stock = mapped_row.get("stock") media_key = mapped_row.get("media_key") # 1. Required Field Checks if not sku: errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "sku", "error_message": "Missing SKU (Item Code)", "suggested_fix": "Provide a unique SKU string in the input row." }) # 2. Price Validation if price: try: p_val = float(price) if p_val < 0: errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "price", "error_message": f"Negative price value: '{price}'", "suggested_fix": "Set price to a non-negative number." }) except ValueError: errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "price", "error_message": f"Invalid numeric price format: '{price}'", "suggested_fix": "Ensure price contains numeric values only." }) # 3. Stock Validation if stock: try: s_val = int(float(stock)) if s_val < 0: errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "stock", "error_message": f"Negative stock count: '{stock}'", "suggested_fix": "Set stock count to 0 or greater." }) except ValueError: errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "stock", "error_message": f"Invalid stock integer format: '{stock}'", "suggested_fix": "Ensure stock is a valid whole number." }) # 4. Check SKU existence in database against import_mode if sku: is_existing = False if existing_sku_set is not None: is_existing = sku in existing_sku_set elif db_session: is_existing = db_session.query(ProductVariant).filter(ProductVariant.sku == sku).first() is not None if is_existing and import_mode == "CREATE_ONLY": errors.append({ "row_number": row_number, "sku": sku, "severity": "ERROR", "field_name": "sku", "error_message": f"SKU '{sku}' already exists, but Import Mode is set to CREATE_ONLY.", "suggested_fix": "Use UPSERT or UPDATE_EXISTING mode to update existing SKUs." }) elif not is_existing and import_mode == "UPDATE_EXISTING": warnings.append({ "row_number": row_number, "sku": sku, "severity": "WARNING", "field_name": "sku", "error_message": f"SKU '{sku}' does not exist in database. Skipping row as Import Mode is UPDATE_EXISTING.", "suggested_fix": "Use UPSERT or CREATE_ONLY mode to create new SKUs." }) # 5. Media Key Verification if media_key: has_media = False if existing_media_key_set is not None: has_media = media_key in existing_media_key_set elif db_session: has_media = db_session.query(MediaGroup).filter(MediaGroup.media_key == media_key).first() is not None if not has_media: warnings.append({ "row_number": row_number, "sku": sku, "severity": "WARNING", "field_name": "media_key", "error_message": f"Media Key '{media_key}' not found in Media Library.", "suggested_fix": "Upload Media.zip containing images for this Media Key prior to import." }) return errors, warnings @classmethod def validate_batch( cls, mapped_rows: List[Tuple[int, Dict[str, Any]]], db_session: Session, import_mode: str = "UPSERT" ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: """ Validates an entire batch of rows, performing row checks and detecting in-file duplicate SKUs. """ all_errors = [] all_warnings = [] seen_skus: Dict[str, int] = {} # sku -> first seen row number # 1. Extract unique SKUs and Media Keys from input rows skus = {row.get("sku") for _, row in mapped_rows if row.get("sku")} media_keys = {row.get("media_key") for _, row in mapped_rows if row.get("media_key")} existing_sku_set = set() existing_media_key_set = set() if db_session: # Batch query existing SKUs in chunks of 5000 sku_list = list(skus) for i in range(0, len(sku_list), 5000): chunk = sku_list[i:i+5000] res = db_session.query(ProductVariant.sku).filter(ProductVariant.sku.in_(chunk)).all() existing_sku_set.update(r[0] for r in res) # Batch query existing Media Keys in chunks of 5000 media_key_list = list(media_keys) for i in range(0, len(media_key_list), 5000): chunk = media_key_list[i:i+5000] res = db_session.query(MediaGroup.media_key).filter(MediaGroup.media_key.in_(chunk)).all() existing_media_key_set.update(r[0] for r in res) # 2. Iterate and validate each row for row_number, mapped_row in mapped_rows: sku = mapped_row.get("sku") # Check in-file duplicate SKU if sku: normalized_sku = str(sku).strip().upper() if normalized_sku in seen_skus: first_row = seen_skus[normalized_sku] all_warnings.append({ "row_number": row_number, "sku": sku, "severity": "WARNING", "field_name": "sku", "error_message": f"Duplicate SKU '{sku}' in file (previously seen at row {first_row}).", "suggested_fix": "Ensure each variant has a unique SKU in the spreadsheet." }) else: seen_skus[normalized_sku] = row_number errors, warnings = cls.validate_row( row_number, mapped_row, db_session, import_mode, existing_sku_set=existing_sku_set, existing_media_key_set=existing_media_key_set ) all_errors.extend(errors) all_warnings.extend(warnings) return all_errors, all_warnings