Spaces:
Sleeping
Sleeping
| """ | |
| Public Tracking API Endpoints | |
| Customer-facing API for installation tracking. | |
| NO AUTHENTICATION REQUIRED - Uses OTP verification instead. | |
| Flow: | |
| 1. POST /request-otp - Customer requests OTP via phone/email | |
| 2. POST /verify-otp - Customer verifies OTP, gets JWT token + order list | |
| 3. GET /{order_id} - Customer views tracking details using JWT token | |
| """ | |
| from fastapi import APIRouter, Depends, HTTPException, status, Header | |
| from sqlalchemy.orm import Session | |
| from typing import Optional | |
| from uuid import UUID | |
| import jwt | |
| from datetime import datetime, timedelta | |
| import logging | |
| from app.api.deps import get_db | |
| from app.config import settings | |
| from app.services.tracking_service import TrackingService | |
| from app.schemas.tracking import ( | |
| TrackingOTPRequest, | |
| TrackingOTPResponse, | |
| TrackingOTPVerify, | |
| TrackingVerifyResponse, | |
| TrackingDetailsResponse | |
| ) | |
| router = APIRouter() | |
| logger = logging.getLogger(__name__) | |
| # JWT settings for tracking tokens | |
| TRACKING_JWT_SECRET = settings.SECRET_KEY | |
| TRACKING_JWT_ALGORITHM = "HS256" | |
| TRACKING_JWT_EXPIRY_HOURS = 24 | |
| # ============================================ | |
| # HELPER FUNCTIONS | |
| # ============================================ | |
| def generate_tracking_jwt(customer_identifier: str) -> str: | |
| """ | |
| Generate JWT token for tracking session. | |
| Args: | |
| customer_identifier: Phone or email verified via OTP | |
| Returns: | |
| JWT token string (24h expiry) | |
| """ | |
| payload = { | |
| "sub": customer_identifier, | |
| "purpose": "tracking", | |
| "exp": datetime.utcnow() + timedelta(hours=TRACKING_JWT_EXPIRY_HOURS), | |
| "iat": datetime.utcnow() | |
| } | |
| token = jwt.encode(payload, TRACKING_JWT_SECRET, algorithm=TRACKING_JWT_ALGORITHM) | |
| return token | |
| def validate_tracking_jwt(authorization: Optional[str] = Header(None)) -> str: | |
| """ | |
| Validate tracking JWT token from Authorization header. | |
| Args: | |
| authorization: Authorization header (Bearer token) | |
| Returns: | |
| Customer identifier from token | |
| Raises: | |
| HTTPException: If token invalid or missing | |
| """ | |
| if not authorization: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Authorization header required" | |
| ) | |
| # Extract token from "Bearer <token>" | |
| parts = authorization.split() | |
| if len(parts) != 2 or parts[0].lower() != "bearer": | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid authorization header format. Expected: Bearer <token>" | |
| ) | |
| token = parts[1] | |
| # Decode and validate token | |
| try: | |
| payload = jwt.decode( | |
| token, | |
| TRACKING_JWT_SECRET, | |
| algorithms=[TRACKING_JWT_ALGORITHM] | |
| ) | |
| # Verify token purpose | |
| if payload.get("purpose") != "tracking": | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid token purpose" | |
| ) | |
| customer_identifier = payload.get("sub") | |
| if not customer_identifier: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid token payload" | |
| ) | |
| return customer_identifier | |
| except jwt.ExpiredSignatureError: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Token has expired. Please request a new OTP." | |
| ) | |
| except jwt.InvalidTokenError as e: | |
| logger.warning(f"Invalid tracking JWT: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid token" | |
| ) | |
| # ============================================ | |
| # PUBLIC ENDPOINTS | |
| # ============================================ | |
| def request_tracking_otp( | |
| request: TrackingOTPRequest | |
| ) -> TrackingOTPResponse: | |
| """ | |
| Request OTP for tracking access (Public endpoint). | |
| Customer provides phone OR email. OTP sent via WhatsApp or email. | |
| No authentication required. | |
| **Flow:** | |
| 1. Customer enters phone/email on tracking page | |
| 2. System sends OTP via chosen delivery method | |
| 3. Customer receives OTP code | |
| **Rate Limiting:** | |
| - OTP service enforces rate limits | |
| - Typically 1 OTP per 60 seconds per identifier | |
| """ | |
| try: | |
| identifier, masked = TrackingService.request_tracking_otp( | |
| phone=request.phone, | |
| email=request.email, | |
| delivery_method=request.delivery_method | |
| ) | |
| return TrackingOTPResponse( | |
| success=True, | |
| message=f"OTP sent successfully via {request.delivery_method}", | |
| masked_identifier=masked | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to send tracking OTP: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Failed to send OTP. Please try again." | |
| ) | |
| def verify_tracking_otp( | |
| request: TrackingOTPVerify, | |
| db: Session = Depends(get_db) | |
| ) -> TrackingVerifyResponse: | |
| """ | |
| Verify OTP and get tracking access token (Public endpoint). | |
| Customer provides OTP code received via WhatsApp/email. | |
| Returns JWT token for tracking session + list of customer's orders. | |
| **Flow:** | |
| 1. Customer enters OTP code | |
| 2. System verifies OTP (uses Redis) | |
| 3. System looks up customer's orders by phone/email | |
| 4. Returns JWT token (24h validity) + order list | |
| **JWT Token:** | |
| - Store token in browser localStorage | |
| - Include in Authorization header for tracking details | |
| - Expires after 24 hours | |
| - No server-side storage (client-side only) | |
| **Security:** | |
| - OTP verification required | |
| - Token tied to specific customer identifier | |
| - Order access validated on each request | |
| """ | |
| try: | |
| # Verify OTP and get customer's orders | |
| orders = TrackingService.verify_tracking_otp( | |
| db=db, | |
| phone=request.phone, | |
| email=request.email, | |
| otp_code=request.otp_code | |
| ) | |
| # Generate tracking JWT token | |
| identifier = request.phone if request.phone else request.email | |
| token = generate_tracking_jwt(identifier) | |
| return TrackingVerifyResponse( | |
| access_token=token, | |
| token_type="bearer", | |
| expires_in=TRACKING_JWT_EXPIRY_HOURS * 3600, # seconds | |
| orders=orders | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to verify tracking OTP: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Verification failed. Please try again." | |
| ) | |
| def get_tracking_details( | |
| order_id: UUID, | |
| customer_identifier: str = Depends(validate_tracking_jwt), | |
| db: Session = Depends(get_db) | |
| ) -> TrackingDetailsResponse: | |
| """ | |
| Get detailed tracking information for an order (Authenticated). | |
| Requires valid tracking JWT token in Authorization header. | |
| Returns complete order, ticket, agent, and journey details. | |
| **Authentication:** | |
| - Authorization: Bearer <tracking_jwt_token> | |
| - Token obtained from /verify-otp endpoint | |
| - Token expires after 24 hours | |
| **Security:** | |
| - Validates order belongs to customer from token | |
| - Prevents unauthorized access to other customers' orders | |
| **Tracking Information:** | |
| - Order details (status, package, price) | |
| - Ticket details (scheduled date/time, progress) | |
| - Agent details (name, phone) if assigned | |
| - Journey tracking (current location, route) if started | |
| **Real-time Updates:** | |
| - Frontend should poll this endpoint every 30-60 seconds | |
| - Location updates when agent is en route | |
| - Status updates when ticket progresses | |
| """ | |
| try: | |
| # Get tracking details (includes security validation) | |
| details = TrackingService.get_tracking_details( | |
| db=db, | |
| order_id=order_id, | |
| customer_identifier=customer_identifier | |
| ) | |
| return TrackingDetailsResponse(**details) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to get tracking details: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Failed to retrieve tracking details" | |
| ) | |
| # ============================================ | |
| # HEALTH CHECK | |
| # ============================================ | |
| def tracking_health_check(): | |
| """ | |
| Health check endpoint for tracking service. | |
| Public endpoint, no authentication required. | |
| Useful for monitoring and frontend connectivity checks. | |
| """ | |
| return { | |
| "status": "healthy", | |
| "service": "customer_tracking", | |
| "timestamp": datetime.utcnow().isoformat() | |
| } | |