206 lines
8.2 KiB
Python
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
|