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 (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
""", [(d["sync_run_id"], d["order_number"], d["status_at_run"]) for d in orders_data])
|
""", [(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 = []
|
all_items = []
|
||||||
order_numbers_with_items = set()
|
order_numbers_with_items = set()
|
||||||
for d in orders_data:
|
for d in orders_data:
|
||||||
for item in d.get("items", []):
|
raw_items = d.get("items", [])
|
||||||
order_numbers_with_items.add(d["order_number"])
|
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((
|
all_items.append((
|
||||||
d["order_number"],
|
d["order_number"],
|
||||||
item.get("sku"), item.get("product_name"),
|
item.get("sku"), item.get("product_name"),
|
||||||
@@ -535,14 +539,40 @@ async def get_web_products_batch(skus: list) -> dict:
|
|||||||
|
|
||||||
# ── order_items ──────────────────────────────────
|
# ── 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):
|
async def add_order_items(order_number: str, items: list):
|
||||||
"""Replace order items — delete any existing rows, then insert fresh batch.
|
"""Replace order items — delete any existing rows, then insert fresh batch.
|
||||||
|
|
||||||
GoMag is source of truth: re-import must reflect quantity changes.
|
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:
|
if not items:
|
||||||
return
|
return
|
||||||
|
items = _dedup_items_by_sku(items)
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,))
|
await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,))
|
||||||
|
|||||||
Reference in New Issue
Block a user