""" @router PosSyncRouter (Backend/app/api/v1/routers/PosSyncRouter.py) Fully Dynamic Database-Driven POS Synchronization Gateway (Zero Hardcoded Data) """ import ulid from datetime import datetime, date from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy import select, func, desc from sqlalchemy.orm import Session from app.core.database.db_session import get_db from app.models.POSTerminalModel import POSTerminal, POSTransactionLog router = APIRouter(prefix="/api/v1/pos", tags=["Offline POS Synchronization"]) class POSTransactionItem(BaseModel): variant_id: str qty: int unit_price: float class POSTransaction(BaseModel): pos_transaction_id: str local_sequence: int invoice_no: str total_amount: float created_at: str items: List[POSTransactionItem] class POSSyncPayload(BaseModel): device_id: str terminal_id: str store_id: str transactions: List[POSTransaction] class RegisterTerminalRequest(BaseModel): terminal_id: str store_name: str ip_address: Optional[str] = "127.0.0.1" @router.post("/terminals/register") def register_pos_terminal(payload: RegisterTerminalRequest, db: Session = Depends(get_db)): """ Dynamically registers or updates a POS terminal. """ terminal = db.execute( select(POSTerminal).where(POSTerminal.terminal_id == payload.terminal_id) ).scalar_one_or_none() if not terminal: terminal = POSTerminal( terminal_id=payload.terminal_id, store_name=payload.store_name, status="ONLINE", ip_address=payload.ip_address, last_heartbeat=datetime.utcnow(), synced_today=0, pending_queue=0, ) db.add(terminal) else: terminal.store_name = payload.store_name terminal.ip_address = payload.ip_address terminal.status = "ONLINE" terminal.last_heartbeat = datetime.utcnow() db.commit() db.refresh(terminal) return { "status": "SUCCESS", "message": f"Terminal '{terminal.terminal_id}' registered successfully.", "terminal_id": terminal.terminal_id, "store_name": terminal.store_name, } @router.post("/sync") def sync_pos_transactions(payload: POSSyncPayload, db: Session = Depends(get_db)): """ Idempotent database-persisted synchronization gateway for offline POS transactions. Dynamically registers unknown terminals upon sync submission. """ terminal = db.execute( select(POSTerminal).where(POSTerminal.terminal_id == payload.terminal_id) ).scalar_one_or_none() if not terminal: terminal = POSTerminal( terminal_id=payload.terminal_id, store_name=payload.store_id or f"Store ({payload.terminal_id})", status="ONLINE", last_heartbeat=datetime.utcnow(), synced_today=0, pending_queue=0, ) db.add(terminal) db.commit() db.refresh(terminal) processed = [] for tx in payload.transactions: # Check idempotency (prevent duplicate syncs) existing = db.execute( select(POSTransactionLog).where( POSTransactionLog.pos_transaction_id == tx.pos_transaction_id ) ).scalar_one_or_none() if existing: processed.append({ "pos_transaction_id": tx.pos_transaction_id, "status": "ALREADY_SYNCED", "invoice_no": tx.invoice_no, }) continue log_entry = POSTransactionLog( sync_id=f"POS-LOG-{str(ulid.ULID())[:8]}", terminal_id=terminal.terminal_id, invoice_no=tx.invoice_no, pos_transaction_id=tx.pos_transaction_id, items_count=len(tx.items), total_amount=tx.total_amount, status="SYNCED", created_at=datetime.utcnow(), ) db.add(log_entry) terminal.synced_today = (terminal.synced_today or 0) + 1 terminal.last_heartbeat = datetime.utcnow() terminal.status = "ONLINE" processed.append({ "pos_transaction_id": tx.pos_transaction_id, "status": "SYNCED", "invoice_no": tx.invoice_no, }) db.commit() return { "status": "SUCCESS", "synced_count": len(processed), "terminal_id": payload.terminal_id, "results": processed, } @router.get("/status") def get_pos_sync_status(db: Session = Depends(get_db)): """ Returns live health telemetry for offline POS sync gateways and store terminals strictly from MySQL DB. """ terminals = db.execute(select(POSTerminal)).scalars().all() today_start = datetime.combine(date.today(), datetime.min.time()) revenue_today = db.execute( select(func.sum(POSTransactionLog.total_amount)).where( POSTransactionLog.created_at >= today_start, POSTransactionLog.status == "SYNCED", ) ).scalar() or 0.0 synced_today_total = db.execute( select(func.count(POSTransactionLog.sync_id)).where( POSTransactionLog.created_at >= today_start, POSTransactionLog.status == "SYNCED", ) ).scalar() or 0 pending_retries = db.execute( select(func.count(POSTransactionLog.sync_id)).where( POSTransactionLog.status == "PENDING_RETRY" ) ).scalar() or 0 terminals_list = [] now = datetime.utcnow() for t in terminals: time_diff = (now - t.last_heartbeat).total_seconds() if t.last_heartbeat else 99999 if time_diff < 60: heartbeat_str = f"{int(time_diff)} seconds ago" elif time_diff < 3600: heartbeat_str = f"{int(time_diff // 60)} minutes ago" else: heartbeat_str = f"{int(time_diff // 3600)} hours ago" terminals_list.append({ "terminal_id": t.terminal_id, "store_name": t.store_name, "status": t.status, "ip_address": t.ip_address or "127.0.0.1", "last_heartbeat": heartbeat_str, "synced_today": t.synced_today, "pending_queue": t.pending_queue, }) last_log = db.execute( select(POSTransactionLog).order_by(desc(POSTransactionLog.created_at)).limit(1) ).scalar_one_or_none() last_sync = last_log.created_at.isoformat() if last_log else None return { "gateway_status": "ONLINE", "active_terminals_count": len(terminals), "synced_today_count": synced_today_total, "pending_retries_count": pending_retries, "offline_revenue_today": round(revenue_today, 2), "last_sync_timestamp": last_sync, "terminals": terminals_list, } @router.get("/logs") def get_pos_sync_logs( terminal_id: Optional[str] = None, status: Optional[str] = None, limit: int = 50, db: Session = Depends(get_db) ): """ Returns real database audit logs for offline POS sync transaction batches. """ stmt = select(POSTransactionLog).order_by(desc(POSTransactionLog.created_at)) if terminal_id: stmt = stmt.where(POSTransactionLog.terminal_id == terminal_id) if status: stmt = stmt.where(POSTransactionLog.status == status) logs = db.execute(stmt.limit(limit)).scalars().all() logs_list = [ { "sync_id": log.sync_id, "terminal_id": log.terminal_id, "invoice_no": log.invoice_no, "pos_transaction_id": log.pos_transaction_id, "items_count": log.items_count, "total_amount": log.total_amount, "status": log.status, "created_at": log.created_at.isoformat(), "error_detail": log.error_detail, } for log in logs ] total_count = db.execute(select(func.count(POSTransactionLog.sync_id))).scalar() or 0 return {"total": total_count, "logs": logs_list} @router.post("/terminals/{terminal_id}/reset") def reset_terminal_sync(terminal_id: str, db: Session = Depends(get_db)): """ Clears pending sync locks or re-syncs state for a given POS terminal in MySQL database. """ terminal = db.execute( select(POSTerminal).where(POSTerminal.terminal_id == terminal_id) ).scalar_one_or_none() if not terminal: raise HTTPException(status_code=404, detail=f"POS Terminal '{terminal_id}' not found") terminal.pending_queue = 0 terminal.status = "ONLINE" terminal.last_heartbeat = datetime.utcnow() db.commit() return { "status": "SUCCESS", "terminal_id": terminal_id, "message": f"Terminal '{terminal_id}' sync state reset successfully in database.", }