diff --git a/src/asquare.py b/src/asquare.py index 6bd106a..1afa8fa 100644 --- a/src/asquare.py +++ b/src/asquare.py @@ -6,6 +6,8 @@ import uuid import re from time import time +from square.core.api_error import ApiError + from . import db from . import get_sq_env @@ -229,20 +231,23 @@ async def update_itm(item_id: str, lego_set: dict, item_data: dict = None): # Convert price to cents for Square API price_in_cents = int(set_adjustedprice * 100) - # ALWAYS fetch fresh item data right before update to get latest version - # The item_data parameter is just used to avoid lookup during enrichment check, - # but we need fresh data for the update to avoid version conflicts - print( - f"[@DEBUG] Fetching fresh item data before update to get current version..." - ) - try: - fresh_item = await get_item_by_id(item_id) - except ValueError as e: - # Item was deleted between webhook and enrichment - print(f"[@WARNING] {e} - skipping update (item may have been deleted)") - return + # Retry logic for version conflicts (race conditions with concurrent updates) + max_retries = 3 + retry_delay = 0.5 # seconds - variations = fresh_item.get("variations", []) + for attempt in range(max_retries): + try: + # ALWAYS fetch fresh item data right before update to get latest version + print( + f"[@DEBUG] Fetching fresh item data (attempt {attempt + 1}/{max_retries})..." + ) + fresh_item = await get_item_by_id(item_id) + except ValueError as e: + # Item was deleted between webhook and enrichment + print(f"[@WARNING] {e} - skipping update (item may have been deleted)") + return + + variations = fresh_item.get("variations", []) if not variations: print( @@ -298,25 +303,46 @@ async def update_itm(item_id: str, lego_set: dict, item_data: dict = None): "item_variation_data" ]["sku"] - print(f"[@DEBUG] Upserting item {item_id} with name: {pw_name}") + print(f"[@DEBUG] Upserting item {item_id} with name: {pw_name}") - # Perform upsert using catalog.object.upsert - result = await client.catalog.object.upsert( - idempotency_key=upsert_body["idempotency_key"], - object=upsert_body["object"], - ) - result_json = json.loads(result.model_dump_json()) + # Perform upsert using catalog.object.upsert + try: + result = await client.catalog.object.upsert( + idempotency_key=upsert_body["idempotency_key"], + object=upsert_body["object"], + ) + result_json = json.loads(result.model_dump_json()) - if result_json.get("catalog_object"): - print(f"[@DEBUG] ✓ Successfully updated item {item_id}") - # TODO: Handle image upload separately (Square Images API) - # This requires multipart/form-data upload - return True - else: - print( - f"[@ERROR] Failed to update item {item_id}: No catalog_object in response" - ) - return False + if result_json.get("catalog_object"): + print(f"[@DEBUG] ✓ Successfully updated item {item_id}") + # TODO: Handle image upload separately (Square Images API) + # This requires multipart/form-data upload + return True + else: + print( + f"[@ERROR] Failed to update item {item_id}: No catalog_object in response" + ) + return False + + except ApiError as e: + # Check if this is a VERSION_MISMATCH error + if e.status_code == 400 and e.body and isinstance(e.body, dict): + errors = e.body.get("errors", []) + if any(err.get("code") == "VERSION_MISMATCH" for err in errors): + if attempt < max_retries - 1: + print( + f"[@WARNING] Version conflict on attempt {attempt + 1}, retrying in {retry_delay}s..." + ) + await asyncio.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + continue # Retry with fresh data + else: + print( + f"[@ERROR] Version conflict persists after {max_retries} attempts, giving up" + ) + return False + # Re-raise other API errors + raise except Exception as e: print(f"[@ERROR] Failed to update item {item_id}: {e}")