ifixkart-backend/Backend/app/services/migration_engine/validation_engine.py

206 lines
8.2 KiB
Python

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