From 80842d9be34c7e4e6111b7d0464bb5210ce27698 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 1 Jun 2026 00:14:43 +0300 Subject: [PATCH] fix(connect): store cloud order id locally to enable reliable status mirroring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 4 _mirror_status_to_cloud function had no way to look up the cloud order's numeric id once it was marked synced (not in the pending list anymore), so status updates from local staff could silently fail. Fix: - models/order.py: online_order_cloud_id INTEGER column added to Order - main.py: migration for the new column - schemas/order.py: online_order_cloud_id exposed in OrderOut - cloud_sync.py: stores cloud_order["id"] as online_order_cloud_id when creating the local order during the pull - connect_orders.py: _mirror_status_to_cloud now takes the integer cloud id directly — no more pending-list lookup; function body reduced from ~50 lines to ~15 Co-Authored-By: Claude Sonnet 4.6 --- local_backend/main.py | 1 + local_backend/models/order.py | 1 + local_backend/routers/connect_orders.py | 70 +++++-------------------- local_backend/schemas/order.py | 1 + local_backend/services/cloud_sync.py | 1 + 5 files changed, 17 insertions(+), 57 deletions(-) diff --git a/local_backend/main.py b/local_backend/main.py index a04cb06..68b9600 100644 --- a/local_backend/main.py +++ b/local_backend/main.py @@ -222,6 +222,7 @@ def _run_migrations(): # Xenia Connect — online order fields on orders table "ALTER TABLE orders ADD COLUMN source VARCHAR NOT NULL DEFAULT 'pos'", "ALTER TABLE orders ADD COLUMN online_order_ref VARCHAR", + "ALTER TABLE orders ADD COLUMN online_order_cloud_id INTEGER", "ALTER TABLE orders ADD COLUMN online_status VARCHAR", "ALTER TABLE orders ADD COLUMN online_customer_name VARCHAR", "ALTER TABLE orders ADD COLUMN online_customer_phone VARCHAR", diff --git a/local_backend/models/order.py b/local_backend/models/order.py index e625b07..2db70fd 100644 --- a/local_backend/models/order.py +++ b/local_backend/models/order.py @@ -23,6 +23,7 @@ class Order(Base): # Xenia Connect — online order fields source = Column(String, default="pos", nullable=False) # "pos" | "online" online_order_ref = Column(String, nullable=True) # e.g. "ORD-0042" + online_order_cloud_id = Column(Integer, nullable=True) # cloud DB pk for status mirroring online_status = Column(String, nullable=True) # mirrors cloud status online_customer_name = Column(String, nullable=True) online_customer_phone = Column(String, nullable=True) diff --git a/local_backend/routers/connect_orders.py b/local_backend/routers/connect_orders.py index e98f951..5445da6 100644 --- a/local_backend/routers/connect_orders.py +++ b/local_backend/routers/connect_orders.py @@ -39,70 +39,26 @@ def _cloud_headers() -> dict: return {"X-Site-ID": settings.SITE_ID, "X-Site-Key": settings.SITE_KEY} -async def _mirror_status_to_cloud(cloud_order_ref: str, new_status: str, rejection_reason: str = None): +async def _mirror_status_to_cloud(cloud_order_id: int, new_status: str, rejection_reason: str = None): """Best-effort push of status update to cloud. Failures are logged, not raised.""" - if not settings.CLOUD_URL or not cloud_order_ref: + if not settings.CLOUD_URL or not cloud_order_id: return try: - # We need the cloud order id — look it up by public_ref via the status endpoint - # (The cloud orders router exposes PATCH /{order_id}/status by numeric id. - # We store cloud id implicitly through public_ref; fetch it first.) + body = {"status": new_status} + if rejection_reason: + body["rejection_reason"] = rejection_reason + async with httpx.AsyncClient(timeout=10) as client: - # Get cloud order id via status lookup (public endpoint, no auth needed) - status_resp = await client.get( - f"{settings.CLOUD_URL}/api/orders/status/{cloud_order_ref}" - ) - if status_resp.status_code != 200: - logger.warning("Could not find cloud order %s", cloud_order_ref) - return - - # Search pending list to get numeric id — use the site-key protected route - from middleware.license_check import license_state - site_numeric_id = license_state.get("site_numeric_id") - if not site_numeric_id: - return - - pending_resp = await client.get( - f"{settings.CLOUD_URL}/api/orders/pending/{site_numeric_id}", - headers=_cloud_headers(), - ) - # The order may already be synced; look in all orders via remote dashboard - # Simpler: use the manager-JWT-free internal PATCH directly via site key - # We need the cloud order numeric id. Store it on first pull — for now, - # the cloud also exposes PATCH via remote dashboard (manager JWT). - # Best path: add a site-key variant. Until then, we mirror via the - # existing PATCH endpoint using site key — works because orders.py - # accepts site key auth on PATCH /{order_id}/status. - # We get the numeric id from the pending list or a dedicated lookup. - # Iterate pending to find our ref (may be empty if already synced). - cloud_order_id = None - if pending_resp.status_code == 200: - for o in pending_resp.json(): - if o.get("public_ref") == cloud_order_ref: - cloud_order_id = o["id"] - break - - if cloud_order_id is None: - # Already synced — not in pending list. We can't easily look it up - # without a dedicated search endpoint. Log and skip for now; the - # remote manager dashboard's own PATCH handles the remote case. - logger.debug("Cloud order %s not in pending list; skipping mirror", cloud_order_ref) - return - - body = {"status": new_status} - if rejection_reason: - body["rejection_reason"] = rejection_reason - - patch_resp = await client.patch( + resp = await client.patch( f"{settings.CLOUD_URL}/api/orders/{cloud_order_id}/status", headers=_cloud_headers(), json=body, ) - patch_resp.raise_for_status() - logger.info("Mirrored status %s for cloud order %s", new_status, cloud_order_ref) + resp.raise_for_status() + logger.info("Mirrored status %s for cloud order id %d", new_status, cloud_order_id) except Exception as e: - logger.warning("Failed to mirror status to cloud for %s: %s", cloud_order_ref, e) + logger.warning("Failed to mirror status to cloud for order id %d: %s", cloud_order_id, e) # ── Endpoints ───────────────────────────────────────────────────────────────── @@ -137,7 +93,7 @@ async def accept_order( db.commit() db.refresh(order) - background.add_task(_mirror_status_to_cloud, order.online_order_ref, "accepted") + background.add_task(_mirror_status_to_cloud, order.online_order_cloud_id, "accepted") broadcast_sync("online_order_updated", {"order_id": order_id, "status": "accepted"}) return order @@ -159,7 +115,7 @@ async def reject_order( db.commit() db.refresh(order) - background.add_task(_mirror_status_to_cloud, order.online_order_ref, "rejected", body.reason) + background.add_task(_mirror_status_to_cloud, order.online_order_cloud_id, "rejected", body.reason) broadcast_sync("online_order_updated", {"order_id": order_id, "status": "rejected"}) return order @@ -183,6 +139,6 @@ async def update_order_status( db.commit() db.refresh(order) - background.add_task(_mirror_status_to_cloud, order.online_order_ref, body.status) + background.add_task(_mirror_status_to_cloud, order.online_order_cloud_id, body.status) broadcast_sync("online_order_updated", {"order_id": order_id, "status": body.status}) return order diff --git a/local_backend/schemas/order.py b/local_backend/schemas/order.py index 3ad246e..53215cf 100644 --- a/local_backend/schemas/order.py +++ b/local_backend/schemas/order.py @@ -122,6 +122,7 @@ class OrderOut(BaseModel): # Xenia Connect — online order fields source: str = "pos" online_order_ref: Optional[str] = None + online_order_cloud_id: Optional[int] = None online_status: Optional[str] = None online_customer_name: Optional[str] = None online_customer_phone: Optional[str] = None diff --git a/local_backend/services/cloud_sync.py b/local_backend/services/cloud_sync.py index 7ccd427..179ee63 100644 --- a/local_backend/services/cloud_sync.py +++ b/local_backend/services/cloud_sync.py @@ -289,6 +289,7 @@ async def _pull_pending_orders(): status="open", source="online", online_order_ref=cloud_order["public_ref"], + online_order_cloud_id=cloud_order["id"], online_status="pending_acceptance", online_customer_name=cloud_order.get("customer_name"), online_customer_phone=cloud_order.get("customer_phone"),