from datetime import datetime, timedelta from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import HTMLResponse from app.broker_store import clear_user_broker from app.services.auth_service import get_user_for_session from app.services.zerodha_service import ( KiteApiError, KiteTokenError, build_login_url, exchange_request_token, fetch_funds, fetch_holdings, ) from app.services.zerodha_storage import ( clear_session, consume_request_token, get_session, set_session, store_request_token, ) router = APIRouter(prefix="/api/zerodha") public_router = APIRouter() def _require_user(request: Request): session_id = request.cookies.get("session_id") if not session_id: raise HTTPException(status_code=401, detail="Not authenticated") user = get_user_for_session(session_id) if not user: raise HTTPException(status_code=401, detail="Not authenticated") return user def _capture_request_token(request: Request, request_token: str): user = _require_user(request) token = request_token.strip() if not token: raise HTTPException(status_code=400, detail="Missing request_token") store_request_token(user["id"], token) def _clear_broker_session(user_id: str): clear_user_broker(user_id) clear_session(user_id) def _raise_kite_error(user_id: str, exc: KiteApiError): if isinstance(exc, KiteTokenError): _clear_broker_session(user_id) raise HTTPException( status_code=401, detail="Zerodha session expired. Please reconnect." ) from exc raise HTTPException(status_code=502, detail=str(exc)) from exc @router.post("/login-url") async def login_url(payload: dict, request: Request): _require_user(request) api_key = (payload.get("apiKey") or "").strip() if not api_key: raise HTTPException(status_code=400, detail="API key is required") return {"loginUrl": build_login_url(api_key)} @router.post("/session") async def create_session(payload: dict, request: Request): user = _require_user(request) api_key = (payload.get("apiKey") or "").strip() api_secret = (payload.get("apiSecret") or "").strip() request_token = (payload.get("requestToken") or "").strip() if not api_key or not api_secret or not request_token: raise HTTPException( status_code=400, detail="API key, secret, and request token are required" ) try: session_data = exchange_request_token(api_key, api_secret, request_token) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc saved = set_session( user["id"], { "api_key": api_key, "access_token": session_data.get("access_token"), "request_token": session_data.get("request_token", request_token), "user_name": session_data.get("user_name"), "broker_user_id": session_data.get("user_id"), }, ) return { "connected": True, "userName": saved.get("user_name"), "brokerUserId": saved.get("broker_user_id"), "accessToken": saved.get("access_token"), } @router.get("/status") async def status(request: Request): user = _require_user(request) session = get_session(user["id"]) if not session: return {"connected": False} return { "connected": True, "broker": "zerodha", "userName": session.get("user_name"), "linkedAt": session.get("linked_at"), } @router.get("/request-token") async def request_token(request: Request): user = _require_user(request) token = consume_request_token(user["id"]) if not token: raise HTTPException(status_code=404, detail="No request token available.") return {"requestToken": token} @router.get("/holdings") async def holdings(request: Request): user = _require_user(request) session = get_session(user["id"]) if not session: raise HTTPException(status_code=400, detail="Zerodha is not connected") try: data = fetch_holdings(session["api_key"], session["access_token"]) except KiteApiError as exc: _raise_kite_error(user["id"], exc) return {"holdings": data} @router.get("/funds") async def funds(request: Request): user = _require_user(request) session = get_session(user["id"]) if not session: raise HTTPException(status_code=400, detail="Zerodha is not connected") try: data = fetch_funds(session["api_key"], session["access_token"]) except KiteApiError as exc: _raise_kite_error(user["id"], exc) equity = data.get("equity", {}) if isinstance(data, dict) else {} return {"funds": {**equity, "raw": data}} @router.get("/equity-curve") async def equity_curve(request: Request, from_: str = Query("", alias="from")): user = _require_user(request) session = get_session(user["id"]) if not session: raise HTTPException(status_code=400, detail="Zerodha is not connected") try: holdings = fetch_holdings(session["api_key"], session["access_token"]) funds_data = fetch_funds(session["api_key"], session["access_token"]) except KiteApiError as exc: _raise_kite_error(user["id"], exc) equity = funds_data.get("equity", {}) if isinstance(funds_data, dict) else {} total_holdings_value = 0 for item in holdings: qty = float(item.get("quantity") or item.get("qty") or 0) last = float(item.get("last_price") or item.get("average_price") or 0) total_holdings_value += qty * last total_funds = float(equity.get("cash") or 0) current_value = max(0, total_holdings_value + total_funds) ms_in_day = 86400000 now = datetime.utcnow() default_start = now - timedelta(days=90) if from_: try: start_date = datetime.fromisoformat(from_) except ValueError: start_date = default_start else: start_date = default_start if start_date > now: start_date = now span_days = max( 2, int(((now - start_date).total_seconds() * 1000) // ms_in_day), ) start_value = current_value * 0.85 if current_value > 0 else 10000 points = [] for i in range(span_days): day = start_date + timedelta(days=i) progress = i / (span_days - 1) trend = start_value + (current_value - start_value) * progress value = max(0, round(trend)) points.append({"date": day.isoformat(), "value": value}) return { "startDate": start_date.isoformat(), "endDate": now.isoformat(), "accountOpenDate": session.get("linked_at"), "points": points, } @router.get("/callback") async def callback(request: Request, request_token: str = ""): _capture_request_token(request, request_token) return { "status": "ok", "message": "Request token captured. You can close this tab.", } @router.get("/login") async def login_redirect(request: Request, request_token: str = ""): return await callback(request, request_token=request_token) @public_router.get("/login", response_class=HTMLResponse) async def login_capture(request: Request, request_token: str = ""): _capture_request_token(request, request_token) return ( "" "

Request token captured

" "

You can close this tab and return to QuantFortune.

" "" )