Add Polaris FastAPI backend: models/schemas, pricing+scoring+order router, importer, Celery tasks, API routers, seed script
This commit is contained in:
115
backend/app/engines/importer.py
Normal file
115
backend/app/engines/importer.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Feed importer: normalize -> validate -> upsert.
|
||||
|
||||
NEVER auto-publishes products: new products are created with status 'imported'
|
||||
only. Invalid items (missing sku/title, non-positive cost, negative inventory)
|
||||
are skipped and logged to audit_logs.
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from app.engines.suppliers.base import build_adapter
|
||||
from app.models import AuditLog, Product, SupplierProduct
|
||||
|
||||
|
||||
def validate(item):
|
||||
problems = []
|
||||
if not (item.get("sku") or "").strip():
|
||||
problems.append("missing sku")
|
||||
if not (item.get("title") or "").strip():
|
||||
problems.append("missing title")
|
||||
try:
|
||||
cost = float(item.get("cost") or 0)
|
||||
if cost <= 0:
|
||||
problems.append("non-positive cost")
|
||||
except (TypeError, ValueError):
|
||||
problems.append("invalid cost")
|
||||
try:
|
||||
inventory = int(float(item.get("inventory") or 0))
|
||||
if inventory < 0:
|
||||
problems.append("negative inventory")
|
||||
except (TypeError, ValueError):
|
||||
problems.append("invalid inventory")
|
||||
return problems
|
||||
|
||||
|
||||
def import_feed(db, supplier, feed_type=None, data_or_url=""):
|
||||
"""Import a supplier feed into products + supplier_products.
|
||||
|
||||
Returns summary dict: {supplier, received, imported, updated, skipped: [...]}
|
||||
"""
|
||||
adapter = build_adapter(supplier, data_or_url=data_or_url)
|
||||
items = adapter.get_products()
|
||||
|
||||
summary = {
|
||||
"supplier": supplier.name,
|
||||
"supplier_id": str(supplier.id),
|
||||
"received": len(items),
|
||||
"imported": 0,
|
||||
"updated": 0,
|
||||
"skipped": [],
|
||||
}
|
||||
|
||||
for item in items:
|
||||
problems = validate(item)
|
||||
if problems:
|
||||
reason = "; ".join(problems)
|
||||
summary["skipped"].append({"sku": item.get("sku"), "reason": reason})
|
||||
db.add(
|
||||
AuditLog(
|
||||
actor="importer",
|
||||
action="import_skipped",
|
||||
entity="product",
|
||||
entity_id=item.get("sku"),
|
||||
detail={"supplier": supplier.name, "reason": reason},
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
product = db.query(Product).filter(Product.sku == item["sku"]).first()
|
||||
if product is None:
|
||||
product = Product(sku=item["sku"], status="imported")
|
||||
db.add(product)
|
||||
summary["imported"] += 1
|
||||
else:
|
||||
summary["updated"] += 1
|
||||
|
||||
# set all required fields BEFORE flushing (title is NOT NULL in schema)
|
||||
product.title = item["title"]
|
||||
product.description = item.get("description") or product.description
|
||||
product.category = item.get("category") or product.category
|
||||
product.brand = item.get("brand") or product.brand
|
||||
product.cost = Decimal(str(item["cost"]))
|
||||
|
||||
if item.get("image_url"):
|
||||
images = list(product.images or [])
|
||||
if item["image_url"] not in images:
|
||||
images.append(item["image_url"])
|
||||
product.images = images
|
||||
|
||||
db.flush() # product.id available now
|
||||
|
||||
sp = (
|
||||
db.query(SupplierProduct)
|
||||
.filter(
|
||||
SupplierProduct.supplier_id == supplier.id,
|
||||
SupplierProduct.supplier_sku == item["sku"],
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if sp is None:
|
||||
sp = SupplierProduct(
|
||||
supplier_id=supplier.id,
|
||||
supplier_sku=item["sku"],
|
||||
product_id=product.id,
|
||||
)
|
||||
db.add(sp)
|
||||
db.flush()
|
||||
else:
|
||||
sp.product_id = product.id
|
||||
|
||||
sp.supplier_cost = Decimal(str(item["cost"]))
|
||||
sp.supplier_inventory = int(item.get("inventory") or 0)
|
||||
sp.shipping_cost = Decimal(str(item.get("shipping_cost") or 0))
|
||||
sp.shipping_time = item.get("shipping_time")
|
||||
|
||||
db.commit()
|
||||
return summary
|
||||
Reference in New Issue
Block a user