fix(sync): dedup order_items by sku before insert to avoid UNIQUE crash
Production sync was failing every minute with: UNIQUE constraint failed: order_items.order_number, order_items.sku GoMag occasionally returns the same SKU on multiple lines within one order (configurable products, promo splits). The order_items PK is (order_number, sku), so the raw batch insert violates UNIQUE and aborts the entire sync — blocking partner-mismatch updates, address refresh, and items repopulation for already-imported orders. Added _dedup_items_by_sku() helper. Applied in save_orders_batch (cancelled/already/skipped paths) and add_order_items (retry/sync import paths). Keeps first price/vat/name, sums quantities on collision. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -193,12 +193,16 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
VALUES (?, ?, ?)
|
||||
""", [(d["sync_run_id"], d["order_number"], d["status_at_run"]) for d in orders_data])
|
||||
|
||||
# 3. Order items — replace semantics (GoMag source of truth)
|
||||
# 3. Order items — replace semantics (GoMag source of truth).
|
||||
# Dedup per-order by SKU (GoMag sometimes returns same SKU twice).
|
||||
all_items = []
|
||||
order_numbers_with_items = set()
|
||||
for d in orders_data:
|
||||
for item in d.get("items", []):
|
||||
order_numbers_with_items.add(d["order_number"])
|
||||
raw_items = d.get("items", [])
|
||||
if not raw_items:
|
||||
continue
|
||||
order_numbers_with_items.add(d["order_number"])
|
||||
for item in _dedup_items_by_sku(raw_items):
|
||||
all_items.append((
|
||||
d["order_number"],
|
||||
item.get("sku"), item.get("product_name"),
|
||||
@@ -535,14 +539,40 @@ async def get_web_products_batch(skus: list) -> dict:
|
||||
|
||||
# ── order_items ──────────────────────────────────
|
||||
|
||||
def _dedup_items_by_sku(items: list) -> list:
|
||||
"""Deduplicate items by SKU within a single order. Sums quantities on collision.
|
||||
GoMag occasionally returns the same SKU on multiple lines (configurable products,
|
||||
promo splits). The order_items primary key is (order_number, sku) so the raw rows
|
||||
would violate UNIQUE. Keeps first price/vat/name; sums quantity + baseprice*qty.
|
||||
"""
|
||||
if not items:
|
||||
return items
|
||||
merged: dict = {}
|
||||
order: list = []
|
||||
for item in items:
|
||||
sku = item.get("sku")
|
||||
if sku is None:
|
||||
order.append(item)
|
||||
continue
|
||||
if sku in merged:
|
||||
prev = merged[sku]
|
||||
prev["quantity"] = (prev.get("quantity") or 0) + (item.get("quantity") or 0)
|
||||
else:
|
||||
merged[sku] = dict(item)
|
||||
order.append(merged[sku])
|
||||
return order
|
||||
|
||||
|
||||
async def add_order_items(order_number: str, items: list):
|
||||
"""Replace order items — delete any existing rows, then insert fresh batch.
|
||||
|
||||
GoMag is source of truth: re-import must reflect quantity changes.
|
||||
Atomic (DELETE + INSERT in one transaction).
|
||||
Atomic (DELETE + INSERT in one transaction). Items with the same SKU are
|
||||
merged (quantities summed) to satisfy the (order_number, sku) PK.
|
||||
"""
|
||||
if not items:
|
||||
return
|
||||
items = _dedup_items_by_sku(items)
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,))
|
||||
|
||||
Reference in New Issue
Block a user