Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, Depends, HTTPException, Request, status, Query | |
| from sqlalchemy.orm import Session | |
| from typing import List, Dict, Any | |
| import uuid | |
| from datetime import datetime, timezone, timedelta | |
| from ..database import get_db, Dish, Order, OrderItem, Person, get_session_db, get_hotel_id_from_request | |
| from ..models.dish import Dish as DishModel | |
| from ..models.order import OrderCreate, Order as OrderModel | |
| from ..models.user import ( | |
| PersonCreate, | |
| PersonLogin, | |
| Person as PersonModel, | |
| PhoneAuthRequest, | |
| PhoneVerifyRequest, | |
| UsernameRequest | |
| ) | |
| from ..services import firebase_auth | |
| from ..middleware import get_session_id | |
| # Demo mode configuration | |
| DEMO_MODE_ENABLED = True # Set to False to disable demo mode | |
| DEMO_CUSTOMER_ID = 999999 | |
| DEMO_CUSTOMER_USERNAME = "Demo Customer" | |
| DEMO_CUSTOMER_PHONE = "+91-DEMO-USER" | |
| router = APIRouter( | |
| prefix="/customer", | |
| tags=["customer"], | |
| responses={404: {"description": "Not found"}}, | |
| ) | |
| # Dependency to get session-aware database | |
| def get_session_database(request: Request): | |
| session_id = get_session_id(request) | |
| return next(get_session_db(session_id)) | |
| # Demo mode endpoint - creates or returns demo customer | |
| def demo_login(request: Request, db: Session = Depends(get_session_database)): | |
| """ | |
| Demo mode login - bypasses authentication and returns a demo customer | |
| """ | |
| if not DEMO_MODE_ENABLED: | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Demo mode is disabled" | |
| ) | |
| hotel_id = get_hotel_id_from_request(request) | |
| # Check if demo customer already exists for this hotel | |
| demo_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.id == DEMO_CUSTOMER_ID | |
| ).first() | |
| if not demo_user: | |
| # Create demo customer | |
| demo_user = Person( | |
| id=DEMO_CUSTOMER_ID, | |
| hotel_id=hotel_id, | |
| username=DEMO_CUSTOMER_USERNAME, | |
| password="demo", # Demo password | |
| phone_number=DEMO_CUSTOMER_PHONE, | |
| visit_count=5, # Show as returning customer | |
| last_visit=datetime.now(timezone.utc), | |
| created_at=datetime.now(timezone.utc) | |
| ) | |
| db.add(demo_user) | |
| db.commit() | |
| db.refresh(demo_user) | |
| else: | |
| # Update last visit time | |
| demo_user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| return { | |
| "success": True, | |
| "message": "Demo login successful", | |
| "user_exists": True, | |
| "user_id": demo_user.id, | |
| "username": demo_user.username, | |
| "demo_mode": True | |
| } | |
| # Get all dishes for menu (only visible ones) | |
| def get_menu(request: Request, category: str = None, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| if category: | |
| # Filter dishes that contain the specified category in their JSON array | |
| import json | |
| all_dishes = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.visibility == 1 | |
| ).all() | |
| filtered_dishes = [] | |
| for dish in all_dishes: | |
| try: | |
| dish_categories = json.loads(dish.category) if dish.category else [] | |
| if isinstance(dish_categories, list) and category in dish_categories: | |
| filtered_dishes.append(dish) | |
| elif isinstance(dish_categories, str) and dish_categories == category: | |
| filtered_dishes.append(dish) | |
| except (json.JSONDecodeError, TypeError): | |
| # Backward compatibility: treat as single category | |
| if dish.category == category: | |
| filtered_dishes.append(dish) | |
| return filtered_dishes | |
| else: | |
| dishes = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.visibility == 1 | |
| ).all() | |
| return dishes | |
| # Get offer dishes (only visible ones) | |
| def get_offers(request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| dishes = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.is_offer == 1, | |
| Dish.visibility == 1 | |
| ).all() | |
| return dishes | |
| # Get special dishes (only visible ones) | |
| def get_specials(request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| dishes = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.is_special == 1, | |
| Dish.visibility == 1 | |
| ).all() | |
| return dishes | |
| # Get all dish categories (only from visible dishes) | |
| def get_categories(request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| categories = db.query(Dish.category).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.visibility == 1 | |
| ).distinct().all() | |
| # Parse JSON categories and flatten them | |
| import json | |
| unique_categories = set() | |
| for category_tuple in categories: | |
| category_str = category_tuple[0] | |
| if category_str: | |
| try: | |
| # Try to parse as JSON array | |
| category_list = json.loads(category_str) | |
| if isinstance(category_list, list): | |
| unique_categories.update(category_list) | |
| else: | |
| unique_categories.add(category_str) | |
| except (json.JSONDecodeError, TypeError): | |
| # If not JSON, treat as single category | |
| unique_categories.add(category_str) | |
| return sorted(list(unique_categories)) | |
| # Register a new user or update existing user | |
| def register_user(user: PersonCreate, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| # Check if user already exists for this hotel | |
| db_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.username == user.username | |
| ).first() | |
| if db_user: | |
| # Update existing user's last visit time (visit count updated only when order is placed) | |
| db_user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| db.refresh(db_user) | |
| return db_user | |
| else: | |
| # Create new user (visit count will be incremented when first order is placed) | |
| db_user = Person( | |
| hotel_id=hotel_id, | |
| username=user.username, | |
| password=user.password, # In a real app, you should hash this password | |
| visit_count=0, | |
| last_visit=datetime.now(timezone.utc), | |
| ) | |
| db.add(db_user) | |
| db.commit() | |
| db.refresh(db_user) | |
| return db_user | |
| # Login user | |
| def login_user(user_data: PersonLogin, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| # Find user by username for this hotel | |
| db_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.username == user_data.username | |
| ).first() | |
| if not db_user: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username" | |
| ) | |
| # Check password (in a real app, you would verify hashed passwords) | |
| if db_user.password != user_data.password: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password" | |
| ) | |
| # Update last visit time (but not visit count - that's only updated when order is placed) | |
| db_user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| # Return user info and a success message | |
| return { | |
| "user": { | |
| "id": db_user.id, | |
| "username": db_user.username, | |
| "visit_count": db_user.visit_count, | |
| }, | |
| "message": "Login successful", | |
| } | |
| # Create new order | |
| def create_order( | |
| order: OrderCreate, request: Request, person_id: int = Query(None), db: Session = Depends(get_session_database) | |
| ): | |
| hotel_id = get_hotel_id_from_request(request) | |
| # If person_id is not provided but we have a username/password, try to find or create the user | |
| if not person_id and hasattr(order, "username") and hasattr(order, "password"): | |
| # Check if user exists for this hotel | |
| db_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.username == order.username | |
| ).first() | |
| if db_user: | |
| # Update existing user's visit count | |
| db_user.visit_count += 1 | |
| db_user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| person_id = db_user.id | |
| else: | |
| # Create new user (visit count starts at 1 since they're placing their first order) | |
| db_user = Person( | |
| hotel_id=hotel_id, | |
| username=order.username, | |
| password=order.password, | |
| visit_count=1, | |
| last_visit=datetime.now(timezone.utc), | |
| ) | |
| db.add(db_user) | |
| db.commit() | |
| db.refresh(db_user) | |
| person_id = db_user.id | |
| elif person_id: | |
| # If person_id is provided (normal flow), increment visit count for that user | |
| db_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.id == person_id | |
| ).first() | |
| if db_user: | |
| db_user.visit_count += 1 | |
| db_user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| # Create order | |
| db_order = Order( | |
| hotel_id=hotel_id, | |
| table_number=order.table_number, | |
| unique_id=order.unique_id, | |
| person_id=person_id, # Link order to person if provided | |
| status="pending", | |
| ) | |
| db.add(db_order) | |
| db.commit() | |
| db.refresh(db_order) | |
| # Mark the table as occupied | |
| from ..database import Table | |
| db_table = db.query(Table).filter( | |
| Table.hotel_id == hotel_id, | |
| Table.table_number == order.table_number | |
| ).first() | |
| if db_table: | |
| db_table.is_occupied = True | |
| db_table.current_order_id = db_order.id | |
| db.commit() | |
| # Create order items | |
| for item in order.items: | |
| # Get the dish to include its information and verify it belongs to this hotel | |
| dish = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.id == item.dish_id | |
| ).first() | |
| if not dish: | |
| continue # Skip if dish doesn't exist or doesn't belong to this hotel | |
| db_item = OrderItem( | |
| hotel_id=hotel_id, | |
| order_id=db_order.id, | |
| dish_id=item.dish_id, | |
| quantity=item.quantity, | |
| price=dish.price, # Store price at time of order | |
| remarks=item.remarks, | |
| ) | |
| db.add(db_item) | |
| db.commit() | |
| db.refresh(db_order) | |
| return db_order | |
| # Get order status | |
| def get_order(order_id: int, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| # Use joinedload to load the dish relationship for each order item | |
| order = db.query(Order).filter( | |
| Order.hotel_id == hotel_id, | |
| Order.id == order_id | |
| ).first() | |
| if order is None: | |
| raise HTTPException(status_code=404, detail="Order not found") | |
| # Explicitly load dish information for each order item | |
| for item in order.items: | |
| if not hasattr(item, "dish") or item.dish is None: | |
| dish = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.id == item.dish_id | |
| ).first() | |
| if dish: | |
| item.dish = dish | |
| return order | |
| # Get orders by person_id | |
| def get_person_orders(person_id: int, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| # Get all orders for a specific person in this hotel | |
| orders = ( | |
| db.query(Order) | |
| .filter( | |
| Order.hotel_id == hotel_id, | |
| Order.person_id == person_id | |
| ) | |
| .order_by(Order.created_at.desc()) | |
| .all() | |
| ) | |
| # Explicitly load dish information for each order item | |
| for order in orders: | |
| for item in order.items: | |
| if not hasattr(item, "dish") or item.dish is None: | |
| dish = db.query(Dish).filter( | |
| Dish.hotel_id == hotel_id, | |
| Dish.id == item.dish_id | |
| ).first() | |
| if dish: | |
| item.dish = dish | |
| return orders | |
| # Request payment for order | |
| def request_payment(order_id: int, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| try: | |
| # Check if order exists and is not already paid | |
| db_order = db.query(Order).filter( | |
| Order.hotel_id == hotel_id, | |
| Order.id == order_id | |
| ).first() | |
| if db_order is None: | |
| raise HTTPException(status_code=404, detail="Order not found") | |
| # Check if order is already paid | |
| if db_order.status == "paid": | |
| return {"message": "Order is already paid"} | |
| # Check if order is completed (ready for payment) | |
| if db_order.status != "completed": | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Order must be completed before payment can be processed" | |
| ) | |
| # Calculate order totals and apply discounts | |
| from ..database import LoyaltyProgram, SelectionOffer, Person | |
| # Calculate subtotal from order items | |
| subtotal = 0 | |
| for item in db_order.items: | |
| if item.dish: | |
| subtotal += item.dish.price * item.quantity | |
| # Initialize discount amounts | |
| loyalty_discount_amount = 0 | |
| loyalty_discount_percentage = 0 | |
| selection_offer_discount_amount = 0 | |
| # Apply loyalty discount if customer is registered | |
| if db_order.person_id: | |
| person = db.query(Person).filter(Person.id == db_order.person_id).first() | |
| if person: | |
| # Get applicable loyalty discount | |
| loyalty_tier = ( | |
| db.query(LoyaltyProgram) | |
| .filter( | |
| LoyaltyProgram.hotel_id == hotel_id, | |
| LoyaltyProgram.visit_count == person.visit_count, | |
| LoyaltyProgram.is_active == True, | |
| ) | |
| .first() | |
| ) | |
| if loyalty_tier: | |
| loyalty_discount_percentage = loyalty_tier.discount_percentage | |
| loyalty_discount_amount = subtotal * (loyalty_discount_percentage / 100) | |
| # Apply selection offer discount | |
| selection_offer = ( | |
| db.query(SelectionOffer) | |
| .filter( | |
| SelectionOffer.hotel_id == hotel_id, | |
| SelectionOffer.min_amount <= subtotal, | |
| SelectionOffer.is_active == True, | |
| ) | |
| .order_by(SelectionOffer.min_amount.desc()) | |
| .first() | |
| ) | |
| if selection_offer: | |
| selection_offer_discount_amount = selection_offer.discount_amount | |
| # Calculate final total after discounts | |
| final_total = subtotal - loyalty_discount_amount - selection_offer_discount_amount | |
| # Ensure final total is not negative | |
| final_total = max(0, final_total) | |
| # Update order with calculated amounts | |
| db_order.status = "paid" | |
| db_order.subtotal_amount = subtotal | |
| db_order.loyalty_discount_amount = loyalty_discount_amount | |
| db_order.loyalty_discount_percentage = loyalty_discount_percentage | |
| db_order.selection_offer_discount_amount = selection_offer_discount_amount | |
| db_order.total_amount = final_total | |
| db_order.updated_at = datetime.now(timezone.utc) | |
| # Check if this is the last unpaid order for this table | |
| from ..database import Table | |
| # Get all orders for this table that are not paid | |
| table_unpaid_orders = db.query(Order).filter( | |
| Order.table_number == db_order.table_number, | |
| Order.status != "paid", | |
| Order.status != "cancelled" | |
| ).all() | |
| # If this is the only unpaid order, mark table as free | |
| if len(table_unpaid_orders) == 1 and table_unpaid_orders[0].id == order_id: | |
| db_table = db.query(Table).filter(Table.table_number == db_order.table_number).first() | |
| if db_table: | |
| db_table.is_occupied = False | |
| db_table.current_order_id = None | |
| db_table.updated_at = datetime.now(timezone.utc) | |
| # Commit the transaction | |
| db.commit() | |
| db.refresh(db_order) | |
| return {"message": "Payment completed successfully", "order_id": order_id} | |
| except HTTPException: | |
| # Re-raise HTTP exceptions | |
| db.rollback() | |
| raise | |
| except Exception as e: | |
| # Handle any other exceptions | |
| db.rollback() | |
| print(f"Error processing payment for order {order_id}: {str(e)}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error processing payment: {str(e)}" | |
| ) | |
| # Cancel order | |
| def cancel_order(order_id: int, request: Request, db: Session = Depends(get_session_database)): | |
| hotel_id = get_hotel_id_from_request(request) | |
| db_order = db.query(Order).filter( | |
| Order.hotel_id == hotel_id, | |
| Order.id == order_id | |
| ).first() | |
| if db_order is None: | |
| raise HTTPException(status_code=404, detail="Order not found") | |
| # Check if order is in pending status (not accepted or completed) | |
| if db_order.status != "pending": | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Only pending orders can be cancelled. Orders that have been accepted by the chef cannot be cancelled." | |
| ) | |
| # Update order status to cancelled | |
| current_time = datetime.now(timezone.utc) | |
| db_order.status = "cancelled" | |
| db_order.updated_at = current_time | |
| # Mark the table as free if this was the current order | |
| from ..database import Table | |
| db_table = db.query(Table).filter(Table.table_number == db_order.table_number).first() | |
| if db_table and db_table.current_order_id == db_order.id: | |
| db_table.is_occupied = False | |
| db_table.current_order_id = None | |
| db_table.updated_at = current_time | |
| db.commit() | |
| return {"message": "Order cancelled successfully"} | |
| # Get person details | |
| def get_person(person_id: int, request: Request, db: Session = Depends(get_session_database)): | |
| person = db.query(Person).filter(Person.id == person_id).first() | |
| if not person: | |
| raise HTTPException(status_code=404, detail="Person not found") | |
| return person | |
| # Phone authentication endpoints | |
| def phone_auth(auth_request: PhoneAuthRequest, request: Request, db: Session = Depends(get_session_database)): | |
| """ | |
| Initiate phone authentication by sending OTP | |
| """ | |
| try: | |
| # Validate phone number format | |
| if not auth_request.phone_number.startswith("+91"): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Phone number must start with +91" | |
| ) | |
| # Send OTP via Firebase | |
| result = firebase_auth.verify_phone_number(auth_request.phone_number) | |
| print(f"Phone auth initiated for: {auth_request.phone_number}, table: {auth_request.table_number}") | |
| return { | |
| "success": True, | |
| "message": "Verification code sent successfully", | |
| "session_info": result.get("sessionInfo", "firebase-verification-token") | |
| } | |
| except HTTPException as e: | |
| print(f"HTTP Exception in phone_auth: {e.detail}") | |
| raise e | |
| except Exception as e: | |
| print(f"Exception in phone_auth: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Failed to send verification code: {str(e)}" | |
| ) | |
| def verify_otp(verify_request: PhoneVerifyRequest, request: Request, db: Session = Depends(get_session_database)): | |
| """ | |
| Verify OTP and authenticate user | |
| """ | |
| try: | |
| print(f"Verifying OTP for phone: {verify_request.phone_number}") | |
| # Verify OTP via Firebase | |
| # Note: The actual OTP verification is done on the client side with Firebase | |
| # This is just a validation step | |
| firebase_auth.verify_otp( | |
| verify_request.phone_number, | |
| verify_request.verification_code | |
| ) | |
| # Check if user exists in database for this hotel | |
| hotel_id = get_hotel_id_from_request(request) | |
| user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.phone_number == verify_request.phone_number | |
| ).first() | |
| if user: | |
| print(f"Existing user found: {user.username}") | |
| # Existing user - update last visit time (visit count updated only when order is placed) | |
| user.last_visit = datetime.now(timezone.utc) | |
| db.commit() | |
| db.refresh(user) | |
| return { | |
| "success": True, | |
| "message": "Authentication successful", | |
| "user_exists": True, | |
| "user_id": user.id, | |
| "username": user.username | |
| } | |
| else: | |
| print(f"New user with phone: {verify_request.phone_number}") | |
| # New user - return flag to collect username | |
| return { | |
| "success": True, | |
| "message": "Authentication successful, but user not found", | |
| "user_exists": False | |
| } | |
| except HTTPException as e: | |
| print(f"HTTP Exception in verify_otp: {e.detail}") | |
| raise e | |
| except Exception as e: | |
| print(f"Exception in verify_otp: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Failed to verify OTP: {str(e)}" | |
| ) | |
| def register_phone_user(user_request: UsernameRequest, request: Request, db: Session = Depends(get_session_database)): | |
| """ | |
| Register a new user after phone authentication | |
| """ | |
| try: | |
| hotel_id = get_hotel_id_from_request(request) | |
| print(f"Registering new user with phone: {user_request.phone_number}, username: {user_request.username}") | |
| # Check if username already exists for this hotel | |
| existing_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.username == user_request.username | |
| ).first() | |
| if existing_user: | |
| print(f"Username already exists: {user_request.username}") | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Username already exists" | |
| ) | |
| # Check if phone number already exists for this hotel | |
| phone_user = db.query(Person).filter( | |
| Person.hotel_id == hotel_id, | |
| Person.phone_number == user_request.phone_number | |
| ).first() | |
| if phone_user: | |
| print(f"Phone number already registered: {user_request.phone_number}") | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Phone number already registered" | |
| ) | |
| # Create new user (visit count will be incremented when first order is placed) | |
| new_user = Person( | |
| hotel_id=hotel_id, | |
| username=user_request.username, | |
| password="", # No password needed for phone auth | |
| phone_number=user_request.phone_number, | |
| visit_count=0, | |
| last_visit=datetime.now(timezone.utc) | |
| ) | |
| db.add(new_user) | |
| db.commit() | |
| db.refresh(new_user) | |
| print(f"User registered successfully: {new_user.id}, {new_user.username}") | |
| return { | |
| "success": True, | |
| "message": "User registered successfully", | |
| "user_id": new_user.id, | |
| "username": new_user.username | |
| } | |
| except HTTPException as e: | |
| print(f"HTTP Exception in register_phone_user: {e.detail}") | |
| raise e | |
| except Exception as e: | |
| print(f"Exception in register_phone_user: {str(e)}") | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Failed to register user: {str(e)}" | |
| ) | |