diff --git a/app/app.py b/app/app.py index 0091284..eb5297d 100644 --- a/app/app.py +++ b/app/app.py @@ -9,7 +9,8 @@ AddStaffSchema, UpdateStaffEmailSchema, UpdateMenuItemSchema, - MenuScanResponse + MenuScanResponse, + CreateOrderSchema ) from .auth import ( authenticate_student, @@ -28,7 +29,8 @@ remove_staff_member, update_staff_email, ) -from .user import get_user_menu +from .user import get_user_menu, create_payment_order +from .webhook import router as webhook_router app = FastAPI() @@ -45,6 +47,8 @@ security = HTTPBearer() +app.include_router(webhook_router) + @app.get("/health", tags=["health"]) def health_check(): return { @@ -67,6 +71,13 @@ async def get_student_menu_endpoint( ): return await get_user_menu(credentials.credentials) +@app.post("/user/order/create", tags=["user"]) +async def create_order_endpoint( + order_data: CreateOrderSchema, + credentials: HTTPAuthorizationCredentials = Security(security) +): + return await create_payment_order(order_data, credentials.credentials) + @app.post('/staff/add-member', tags=["manager"]) async def add_staff_endpoint( staff_data: AddStaffSchema, diff --git a/app/schema.py b/app/schema.py index 45e47bd..d22d912 100644 --- a/app/schema.py +++ b/app/schema.py @@ -42,3 +42,11 @@ class MenuScanResponse(BaseModel): detected_items: List[ExtractedMenuItem] count: int message: str = "Scan complete. Please verify items before saving." + +class CartItemSchema(BaseModel): + item_id: str + quantity: int = Field(..., gt=0) + +class CreateOrderSchema(BaseModel): + stall_id: str + items: List[CartItemSchema] diff --git a/app/user.py b/app/user.py index 342a105..b3cef55 100644 --- a/app/user.py +++ b/app/user.py @@ -1,11 +1,18 @@ #app/user.py +import os +import razorpay from fastapi.responses import JSONResponse from starlette import status from firebase_admin import auth from .firebase_init import db from datetime import datetime +from .schema import CreateOrderSchema +razorpay_client = razorpay.Client(auth=( + os.environ.get("RAZORPAY_KEY_ID"), + os.environ.get("RAZORPAY_KEY_SECRET") +)) async def get_user_details(id_token: str): try: @@ -95,3 +102,82 @@ async def get_user_menu(id_token: str): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"message": str(e)} ) + + +async def create_payment_order(order_data: CreateOrderSchema, id_token: str): + try: + user_data, user_uid = await get_user_details(id_token) + if not user_data: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"message": "Invalid or expired token."} + ) + + college_id = user_data.get("college_id") + stall_id = order_data.stall_id + + total_amount = 0 + + menu_ref = ( + db.collection("colleges") + .document(college_id) + .collection("stalls") + .document(stall_id) + .collection("menu_items") + ) + + for cart_item in order_data.items: + item_doc = menu_ref.document(cart_item.item_id).get() + + if item_doc.exists: + item_data = item_doc.to_dict() + + if item_data.get('is_available') is False: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"message": f"Sorry, {item_data.get('name')} is currently out of stock."} + ) + + price = item_data.get('price', 0) + total_amount += price * cart_item.quantity + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"message": "One or more items in your cart no longer exist."} + ) + + if total_amount <= 0: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"message": "Invalid order total (0). Check item availability."} + ) + + data = { + "amount": int(total_amount * 100), + "currency": "INR", + "receipt": f"order_{user_uid[:5]}_{int(datetime.now().timestamp())}", + "notes": { + "stall_id": stall_id, + "user_uid": user_uid, + "college_id": college_id + } + } + + order = razorpay_client.order.create(data=data) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "id": order['id'], + "amount": order['amount'], + "currency": order['currency'], + "key_id": os.environ.get("RAZORPAY_KEY_ID"), + "message": "Order created successfully" + } + ) + + except Exception as e: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"message": f"Payment Error: {str(e)}"} + ) diff --git a/app/webhook.py b/app/webhook.py new file mode 100644 index 0000000..4571168 --- /dev/null +++ b/app/webhook.py @@ -0,0 +1,66 @@ +# app/webhook.py +import os +import hmac +import hashlib +import json +from fastapi import APIRouter, Request, HTTPException +from firebase_admin import firestore +from .firebase_init import db + +router = APIRouter() + + +@router.post("/webhook/razorpay", tags=["webhook"]) +async def razorpay_webhook(request: Request): + signature = request.headers.get('X-Razorpay-Signature') + secret = os.environ.get("RAZORPAY_WEBHOOK_SECRET") + body = await request.body() + + try: + expected_signature = hmac.new( + key=secret.encode(), + msg=body, + digestmod=hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(expected_signature, signature): + raise HTTPException(status_code=400, detail="Invalid signature") + except Exception: + raise HTTPException(status_code=400, detail="Signature verification failed") + + payload = await request.json() + event_type = payload.get('event') + + if event_type in ['payment.captured', 'payment_link.paid']: + payment = payload['payload']['payment']['entity'] + notes = payment.get('notes', {}) + + if isinstance(notes, list): + notes = {} + + stall_id = notes.get('stall_id') + user_id = notes.get('user_uid') + + if not stall_id: + stall_id = "manual_test_stall" + + order_data = { + "order_id": payment['order_id'], + "payment_id": payment['id'], + "amount": payment['amount'] / 100, + "status": "PAID", + "stall_id": stall_id, + "user_id": user_id, + "created_at": firestore.SERVER_TIMESTAMP + } + + try: + db.collection('orders').add(order_data) + except Exception as e: + print(f"Error saving order to Firestore: {e}") + raise HTTPException(status_code=500, detail="Failed to save order") + + else: + print(f"⏭️ Skipping event: {event_type}") + + return {"status": "ok"} diff --git a/requirements.txt b/requirements.txt index 3b7a831..7686fb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,6 +50,7 @@ pyparsing==3.2.5 python-dotenv==1.2.1 python-jwt==4.1.0 python-multipart==0.0.21 +razorpay==2.0.0 requests==2.32.5 requests-toolbelt==0.10.1 rsa==4.9.1 @@ -60,5 +61,5 @@ tqdm==4.67.1 typing-inspection==0.4.2 typing_extensions==4.15.0 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==1.26.20 uvicorn==0.40.0