Spaces:
Sleeping
Sleeping
saadrizvi09
commited on
Commit
Β·
9d905d6
1
Parent(s):
21793ce
websocket fix
Browse files- routers/orders.py +58 -9
- routers/ws.py +13 -0
- schema.sql +3 -1
routers/orders.py
CHANGED
|
@@ -30,6 +30,19 @@ router = APIRouter(tags=["orders"])
|
|
| 30 |
|
| 31 |
# In-memory payment auto-unlock timers
|
| 32 |
_payment_timers: dict[str, asyncio.Task] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -173,10 +186,11 @@ async def add_to_cart(data: AddToCartRequest):
|
|
| 173 |
new_ver = cart.data[0]["version"] + 1
|
| 174 |
sb.table("carts").update({"version": new_ver, "updated_at": datetime.now(timezone.utc).isoformat()}).eq("id", cart_id).execute()
|
| 175 |
|
|
|
|
| 176 |
result = _enrich_cart(sb, data.session_id)
|
| 177 |
|
| 178 |
-
# Broadcast
|
| 179 |
-
|
| 180 |
|
| 181 |
return result
|
| 182 |
|
|
@@ -190,7 +204,7 @@ async def remove_from_cart(data: RemoveFromCartRequest):
|
|
| 190 |
|
| 191 |
sb.table("cart_items").delete().eq("id", data.cart_item_id).execute()
|
| 192 |
result = _enrich_cart(sb, data.session_id)
|
| 193 |
-
|
| 194 |
return result
|
| 195 |
|
| 196 |
|
|
@@ -207,7 +221,7 @@ async def update_cart_quantity(data: UpdateCartQtyRequest):
|
|
| 207 |
sb.table("cart_items").update({"quantity": data.quantity}).eq("id", data.cart_item_id).execute()
|
| 208 |
|
| 209 |
result = _enrich_cart(sb, data.session_id)
|
| 210 |
-
|
| 211 |
return result
|
| 212 |
|
| 213 |
|
|
@@ -284,13 +298,19 @@ async def get_session_status(session_id: str):
|
|
| 284 |
.execute()
|
| 285 |
)
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
return {
|
| 288 |
"session_status": s["status"],
|
| 289 |
"payment_lock": s.get("payment_lock", False),
|
| 290 |
"payment_lock_at": s.get("payment_lock_at"),
|
|
|
|
| 291 |
"chef_eta_minutes": s.get("chef_eta_minutes"),
|
| 292 |
"chef_eta_set_at": s.get("chef_eta_set_at"),
|
| 293 |
"order": order.data[0] if order.data else None,
|
|
|
|
| 294 |
}
|
| 295 |
|
| 296 |
|
|
@@ -308,10 +328,12 @@ async def lock_payment(data: PaymentLockRequest):
|
|
| 308 |
raise HTTPException(status_code=423, detail="Payment already locked")
|
| 309 |
|
| 310 |
now = datetime.now(timezone.utc).isoformat()
|
| 311 |
-
sb.
|
| 312 |
"payment_lock": True,
|
| 313 |
"payment_lock_at": now,
|
| 314 |
-
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# Broadcast lock
|
| 317 |
await ws_manager.broadcast(data.session_id, {
|
|
@@ -326,7 +348,8 @@ async def lock_payment(data: PaymentLockRequest):
|
|
| 326 |
sb2 = get_supabase()
|
| 327 |
s = sb2.table("sessions").select("payment_lock, status").eq("id", data.session_id).execute()
|
| 328 |
if s.data and s.data[0].get("payment_lock") and s.data[0]["status"] == "active":
|
| 329 |
-
sb2.
|
|
|
|
| 330 |
await ws_manager.broadcast(data.session_id, {"type": "payment_unlocked"})
|
| 331 |
|
| 332 |
if data.session_id in _payment_timers:
|
|
@@ -336,6 +359,30 @@ async def lock_payment(data: PaymentLockRequest):
|
|
| 336 |
return {"message": "Cart locked for payment", "timeout_seconds": 120}
|
| 337 |
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
@router.post("/payment/confirm")
|
| 340 |
async def confirm_payment(data: PaymentConfirmRequest):
|
| 341 |
sb = get_supabase()
|
|
@@ -407,10 +454,12 @@ async def confirm_payment(data: PaymentConfirmRequest):
|
|
| 407 |
}).execute()
|
| 408 |
|
| 409 |
# Complete session
|
| 410 |
-
sb.
|
| 411 |
"status": "completed",
|
| 412 |
"payment_lock": False,
|
| 413 |
-
|
|
|
|
|
|
|
| 414 |
|
| 415 |
# Table β dirty
|
| 416 |
sb.table("restaurant_tables").update({"status": "dirty"}).eq("id", s["table_id"]).execute()
|
|
|
|
| 30 |
|
| 31 |
# In-memory payment auto-unlock timers
|
| 32 |
_payment_timers: dict[str, asyncio.Task] = {}
|
| 33 |
+
# In-memory payment lock owner tracking (session_id β participant_id)
|
| 34 |
+
_payment_lock_owners: dict[str, str] = {}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _safe_session_update(sb, session_id: str, fields: dict):
|
| 38 |
+
"""Update session, retrying without new columns if they don't exist yet."""
|
| 39 |
+
try:
|
| 40 |
+
sb.table("sessions").update(fields).eq("id", session_id).execute()
|
| 41 |
+
except Exception:
|
| 42 |
+
# Fallback: strip columns that may not exist in DB yet
|
| 43 |
+
safe = {k: v for k, v in fields.items() if k in ("status", "payment_lock")}
|
| 44 |
+
if safe:
|
| 45 |
+
sb.table("sessions").update(safe).eq("id", session_id).execute()
|
| 46 |
|
| 47 |
|
| 48 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 186 |
new_ver = cart.data[0]["version"] + 1
|
| 187 |
sb.table("carts").update({"version": new_ver, "updated_at": datetime.now(timezone.utc).isoformat()}).eq("id", cart_id).execute()
|
| 188 |
|
| 189 |
+
# Enrich cart once
|
| 190 |
result = _enrich_cart(sb, data.session_id)
|
| 191 |
|
| 192 |
+
# Broadcast in background (non-blocking)
|
| 193 |
+
asyncio.create_task(ws_manager.broadcast(data.session_id, {"type": "cart_update", "cart": result}))
|
| 194 |
|
| 195 |
return result
|
| 196 |
|
|
|
|
| 204 |
|
| 205 |
sb.table("cart_items").delete().eq("id", data.cart_item_id).execute()
|
| 206 |
result = _enrich_cart(sb, data.session_id)
|
| 207 |
+
asyncio.create_task(ws_manager.broadcast(data.session_id, {"type": "cart_update", "cart": result}))
|
| 208 |
return result
|
| 209 |
|
| 210 |
|
|
|
|
| 221 |
sb.table("cart_items").update({"quantity": data.quantity}).eq("id", data.cart_item_id).execute()
|
| 222 |
|
| 223 |
result = _enrich_cart(sb, data.session_id)
|
| 224 |
+
asyncio.create_task(ws_manager.broadcast(data.session_id, {"type": "cart_update", "cart": result}))
|
| 225 |
return result
|
| 226 |
|
| 227 |
|
|
|
|
| 298 |
.execute()
|
| 299 |
)
|
| 300 |
|
| 301 |
+
# Count participants for split bill
|
| 302 |
+
participants = sb.table("participants").select("id").eq("session_id", session_id).execute()
|
| 303 |
+
participant_count = len(participants.data) if participants.data else 1
|
| 304 |
+
|
| 305 |
return {
|
| 306 |
"session_status": s["status"],
|
| 307 |
"payment_lock": s.get("payment_lock", False),
|
| 308 |
"payment_lock_at": s.get("payment_lock_at"),
|
| 309 |
+
"payment_locked_by": s.get("payment_locked_by") or _payment_lock_owners.get(session_id),
|
| 310 |
"chef_eta_minutes": s.get("chef_eta_minutes"),
|
| 311 |
"chef_eta_set_at": s.get("chef_eta_set_at"),
|
| 312 |
"order": order.data[0] if order.data else None,
|
| 313 |
+
"participant_count": participant_count,
|
| 314 |
}
|
| 315 |
|
| 316 |
|
|
|
|
| 328 |
raise HTTPException(status_code=423, detail="Payment already locked")
|
| 329 |
|
| 330 |
now = datetime.now(timezone.utc).isoformat()
|
| 331 |
+
_safe_session_update(sb, data.session_id, {
|
| 332 |
"payment_lock": True,
|
| 333 |
"payment_lock_at": now,
|
| 334 |
+
"payment_locked_by": data.participant_id,
|
| 335 |
+
})
|
| 336 |
+
_payment_lock_owners[data.session_id] = data.participant_id
|
| 337 |
|
| 338 |
# Broadcast lock
|
| 339 |
await ws_manager.broadcast(data.session_id, {
|
|
|
|
| 348 |
sb2 = get_supabase()
|
| 349 |
s = sb2.table("sessions").select("payment_lock, status").eq("id", data.session_id).execute()
|
| 350 |
if s.data and s.data[0].get("payment_lock") and s.data[0]["status"] == "active":
|
| 351 |
+
_safe_session_update(sb2, data.session_id, {"payment_lock": False, "payment_lock_at": None, "payment_locked_by": None})
|
| 352 |
+
_payment_lock_owners.pop(data.session_id, None)
|
| 353 |
await ws_manager.broadcast(data.session_id, {"type": "payment_unlocked"})
|
| 354 |
|
| 355 |
if data.session_id in _payment_timers:
|
|
|
|
| 359 |
return {"message": "Cart locked for payment", "timeout_seconds": 120}
|
| 360 |
|
| 361 |
|
| 362 |
+
@router.post("/payment/unlock")
|
| 363 |
+
async def unlock_payment(data: PaymentLockRequest):
|
| 364 |
+
"""Unlock payment (back button) β only the locker can unlock."""
|
| 365 |
+
sb = get_supabase()
|
| 366 |
+
session = sb.table("sessions").select("*").eq("id", data.session_id).execute()
|
| 367 |
+
if not session.data:
|
| 368 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 369 |
+
if not session.data[0].get("payment_lock"):
|
| 370 |
+
return {"message": "Already unlocked"}
|
| 371 |
+
|
| 372 |
+
_safe_session_update(sb, data.session_id, {
|
| 373 |
+
"payment_lock": False,
|
| 374 |
+
"payment_lock_at": None,
|
| 375 |
+
"payment_locked_by": None,
|
| 376 |
+
})
|
| 377 |
+
_payment_lock_owners.pop(data.session_id, None)
|
| 378 |
+
if data.session_id in _payment_timers:
|
| 379 |
+
_payment_timers[data.session_id].cancel()
|
| 380 |
+
del _payment_timers[data.session_id]
|
| 381 |
+
|
| 382 |
+
await ws_manager.broadcast(data.session_id, {"type": "payment_unlocked"})
|
| 383 |
+
return {"message": "Payment unlocked"}
|
| 384 |
+
|
| 385 |
+
|
| 386 |
@router.post("/payment/confirm")
|
| 387 |
async def confirm_payment(data: PaymentConfirmRequest):
|
| 388 |
sb = get_supabase()
|
|
|
|
| 454 |
}).execute()
|
| 455 |
|
| 456 |
# Complete session
|
| 457 |
+
_safe_session_update(sb, data.session_id, {
|
| 458 |
"status": "completed",
|
| 459 |
"payment_lock": False,
|
| 460 |
+
"payment_locked_by": None,
|
| 461 |
+
})
|
| 462 |
+
_payment_lock_owners.pop(data.session_id, None)
|
| 463 |
|
| 464 |
# Table β dirty
|
| 465 |
sb.table("restaurant_tables").update({"status": "dirty"}).eq("id", s["table_id"]).execute()
|
routers/ws.py
CHANGED
|
@@ -18,6 +18,14 @@ from supabase_client import get_supabase
|
|
| 18 |
router = APIRouter()
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# ββ Shared helpers: build state payloads βββββββββββββββββββ
|
| 23 |
|
|
@@ -80,12 +88,17 @@ def _get_session_state(session_id: str) -> dict:
|
|
| 80 |
.limit(1)
|
| 81 |
.execute()
|
| 82 |
)
|
|
|
|
|
|
|
|
|
|
| 83 |
return {
|
| 84 |
"session_status": s["status"],
|
| 85 |
"payment_lock": s.get("payment_lock", False),
|
|
|
|
| 86 |
"chef_eta_minutes": s.get("chef_eta_minutes"),
|
| 87 |
"chef_eta_set_at": s.get("chef_eta_set_at"),
|
| 88 |
"order": order.data[0] if order.data else None,
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
|
|
|
|
| 18 |
router = APIRouter()
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
| 21 |
+
# Import in-memory lock owners from orders module (lazy to avoid circular)
|
| 22 |
+
def _get_lock_owner(session_id: str) -> str | None:
|
| 23 |
+
try:
|
| 24 |
+
from routers.orders import _payment_lock_owners
|
| 25 |
+
return _payment_lock_owners.get(session_id)
|
| 26 |
+
except Exception:
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
|
| 30 |
# ββ Shared helpers: build state payloads βββββββββββββββββββ
|
| 31 |
|
|
|
|
| 88 |
.limit(1)
|
| 89 |
.execute()
|
| 90 |
)
|
| 91 |
+
# Count participants
|
| 92 |
+
participants = sb.table("participants").select("id", count="exact").eq("session_id", session_id).execute()
|
| 93 |
+
participant_count = participants.count if participants.count else 1
|
| 94 |
return {
|
| 95 |
"session_status": s["status"],
|
| 96 |
"payment_lock": s.get("payment_lock", False),
|
| 97 |
+
"payment_locked_by": s.get("payment_locked_by") or _get_lock_owner(session_id),
|
| 98 |
"chef_eta_minutes": s.get("chef_eta_minutes"),
|
| 99 |
"chef_eta_set_at": s.get("chef_eta_set_at"),
|
| 100 |
"order": order.data[0] if order.data else None,
|
| 101 |
+
"participant_count": participant_count,
|
| 102 |
}
|
| 103 |
|
| 104 |
|
schema.sql
CHANGED
|
@@ -244,7 +244,9 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
| 244 |
status session_status DEFAULT 'active',
|
| 245 |
created_at TIMESTAMPTZ DEFAULT now(),
|
| 246 |
expires_at TIMESTAMPTZ,
|
| 247 |
-
payment_lock
|
|
|
|
|
|
|
| 248 |
FOREIGN KEY (table_id) REFERENCES restaurant_tables(id),
|
| 249 |
FOREIGN KEY (restaurant_id) REFERENCES restaurants(id)
|
| 250 |
);
|
|
|
|
| 244 |
status session_status DEFAULT 'active',
|
| 245 |
created_at TIMESTAMPTZ DEFAULT now(),
|
| 246 |
expires_at TIMESTAMPTZ,
|
| 247 |
+
payment_lock BOOLEAN DEFAULT FALSE,
|
| 248 |
+
payment_locked_by TEXT,
|
| 249 |
+
payment_lock_at TIMESTAMPTZ,
|
| 250 |
FOREIGN KEY (table_id) REFERENCES restaurant_tables(id),
|
| 251 |
FOREIGN KEY (restaurant_id) REFERENCES restaurants(id)
|
| 252 |
);
|