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:
Claude Agent
2026-04-22 07:04:49 +00:00
parent 404bc094cd
commit 51790accf9

View File

@@ -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,))