diff --git a/app/app.py b/app/app.py index db13543..898ceef 100644 --- a/app/app.py +++ b/app/app.py @@ -46,7 +46,10 @@ create_payment_order, get_user_orders, verify_payment_and_update_order, - update_user_profile + update_user_profile, + cancel_order, + get_discounted_feed, + buy_resale_item ) from .webhook import router as webhook_router @@ -104,6 +107,12 @@ async def get_student_menu_endpoint( ): return await get_user_menu(credentials.credentials) +@app.get("/user/feed/discounted", tags=["user"]) +async def get_discounted_feed_endpoint( + credentials: HTTPAuthorizationCredentials = Security(security) +): + return await get_discounted_feed(credentials.credentials) + @app.post("/user/order/create", tags=["user"]) async def create_order_endpoint( order_data: CreateOrderSchema, @@ -124,6 +133,20 @@ async def verify_order_endpoint( ): return await verify_payment_and_update_order( payment_data, credentials.credentials ) +@app.post("/user/order/{order_id}/cancel", tags=["user"]) +async def cancel_order_endpoint( + order_id: str, + credentials: HTTPAuthorizationCredentials = Security(security) +): + return await cancel_order(order_id, credentials.credentials) + +@app.post("/user/resale/{resale_id}/buy", tags=["user"]) +async def buy_resale_item_endpoint( + resale_id: str, + credentials: HTTPAuthorizationCredentials = Security(security) +): + return await buy_resale_item(resale_id, credentials.credentials) + @app.get("/staff/performance/overview", tags=["manager"]) async def get_stall_performance_overview_endpoint( month: int, diff --git a/app/manager.py b/app/manager.py index 7010729..afc2056 100644 --- a/app/manager.py +++ b/app/manager.py @@ -24,8 +24,6 @@ async def get_my_staff(id_token: str): staff_list = [] for doc in staff_query: data = doc.to_dict() - # if data.get("role") == "manager": - # continue staff_list.append({ "uid": doc.id, diff --git a/app/schema.py b/app/schema.py index 6f82044..aba0c4a 100644 --- a/app/schema.py +++ b/app/schema.py @@ -103,4 +103,16 @@ class StaffStats(BaseModel): class StallPerformanceResponse(BaseModel): stall_id: str period: str - staff_stats: List[StaffStats] \ No newline at end of file + staff_stats: List[StaffStats] + +class ResaleItemSchema(BaseModel): + resale_id: str + items: List[dict] + original_price: float + discounted_price: float + stall_name: str + status: str + +class CancelOrderResponse(BaseModel): + message: str + resale_created: bool = False diff --git a/app/user.py b/app/user.py index d6e97ac..a0db0c7 100644 --- a/app/user.py +++ b/app/user.py @@ -7,7 +7,7 @@ from starlette import status from firebase_admin import auth from .firebase_init import db, firestore -from datetime import datetime +from datetime import datetime, timedelta from .schema import CreateOrderSchema, UpdateUserProfileSchema, VerifyPaymentSchema razorpay_client = razorpay.Client(auth=( @@ -354,3 +354,248 @@ def normalize_order_status(status:str): "READY": "Ready", "COMPLETED": "Completed" }.get(status,"Unknown") + +async def cancel_order(order_id: str, 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": "Unauthorized"}) + + now = datetime.now() + + week_start = user_data.get("cancellation_week_start") + current_count = user_data.get("cancellations_this_week", 0) + + if week_start: + if isinstance(week_start, str): + week_start = datetime.fromisoformat(week_start) + else: + week_start = now + + if (now.replace(tzinfo=None) - week_start.replace(tzinfo=None)).days >= 7: + current_count = 0 + week_start = now + db.collection("users").document(user_uid).update({ + "cancellation_week_start": firestore.SERVER_TIMESTAMP, + "cancellations_this_week": 0 + }) + + if current_count >= 2: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"message": "Cancellation limit reached. You can only cancel 2 orders per week."} + ) + + order_ref = db.collection("orders").document(order_id) + order_doc = order_ref.get() + + if not order_doc.exists: + return JSONResponse(status_code=404, content={"message": "Order not found"}) + + order_data = order_doc.to_dict() + + if order_data.get("user_id") != user_uid: + return JSONResponse(status_code=403, content={"message": "You do not own this order"}) + + current_status = order_data.get("status") + + if current_status in ["CLAIMED", "COMPLETED", "CANCELLED"]: + return JSONResponse( + status_code=400, + content={"message": f"Cannot cancel order with status: {current_status}"} + ) + + resale_created = False + + if current_status == "READY": + college_id = order_data.get("college_id") + stall_id = order_data.get("stall_id") + original_price = order_data.get("total_amount", 0) + + discounted_price = original_price * 0.5 + + resale_item = { + "original_order_id": order_id, + "original_user_id": user_uid, + "college_id": college_id, + "stall_id": stall_id, + "stall_name": order_data.get("stall_name"), + "items": order_data.get("items", []), + "original_price": original_price, + "discounted_price": discounted_price, + "status": "AVAILABLE", + "created_at": firestore.SERVER_TIMESTAMP + } + + db.collection("resale_items").add(resale_item) + resale_created = True + + batch = db.batch() + + batch.update(order_ref, { + "status": "CANCELLED", + "cancelled_at": firestore.SERVER_TIMESTAMP, + "cancellation_reason": "User requested" + }) + + user_ref = db.collection("users").document(user_uid) + batch.update(user_ref, { + "cancellations_this_week": current_count + 1, + "cancellation_week_start": week_start + }) + + batch.commit() + + msg = "Order cancelled." + if resale_created: + msg += " Item has been added to the discounted feed." + + return JSONResponse(status_code=200, content={"message": msg, "resale_created": resale_created}) + + except Exception as e: + return JSONResponse(status_code=500, content={"message": str(e)}) + +async def buy_resale_item(resale_id: str, id_token: str): + try: + user_data, user_uid = await get_user_details(id_token) + if not user_data: + return JSONResponse(status_code=401, content={"message": "Unauthorized"}) + + resale_ref = db.collection("resale_items").document(resale_id) + + if resale_ref.get("original_user_id") == user_uid: + raise Exception("You cannot purchase your own cancelled order.") + + transaction = db.transaction() + + @firestore.transactional + def reserve_item_transaction(transaction, resale_ref): + snapshot = resale_ref.get(transaction=transaction) + + if not snapshot.exists: + raise Exception("Item not found") + + data = snapshot.to_dict() + current_status = data.get("status") + last_updated = data.get("reserved_at") + + is_available = (current_status == "AVAILABLE") + + if current_status == "RESERVED" and last_updated: + reservation_time = last_updated + if now - reservation_time.replace(tzinfo=None) > timedelta(minutes=5): + is_available = True + + if not is_available: + raise Exception("Item is currently being purchased by someone else.") + + transaction.update(resale_ref, { + "status": "RESERVED", + "reserved_by": user_uid, + "reserved_at": firestore.SERVER_TIMESTAMP + }) + + return data + + try: + now = datetime.now() + resale_data = reserve_item_transaction(transaction, resale_ref) + + discounted_price = resale_data.get("discounted_price", 0) + + user_snapshot = { + "name": user_data.get("name", "Unknown"), + "phone": user_data.get("phone", "") + } + + new_order_ref = db.collection('orders').document() + internal_order_id = new_order_ref.id + + firestore_order_data = { + "user_id": user_uid, + "user_details": user_snapshot, + "stall_id": resale_data.get("stall_id"), + "stall_name": resale_data.get("stall_name"), + "college_id": resale_data.get("college_id"), + "items": resale_data.get("items", []), + "total_amount": discounted_price, + "status": "PENDING", + "order_type": "RESALE", + "resale_item_ref": resale_id, + "created_at": firestore.SERVER_TIMESTAMP, + "updated_at": firestore.SERVER_TIMESTAMP + } + + payment_payload = { + "amount": int(discounted_price * 100), + "currency": "INR", + "receipt": f"resale_{internal_order_id[:8]}", + "notes": { + "stall_id": resale_data.get("stall_id"), + "user_uid": user_uid, + "college_id": resale_data.get("college_id"), + "internal_order_id": internal_order_id, + "type": "RESALE", + "resale_item_id": resale_id + } + } + + razorpay_order = razorpay_client.order.create(data=payment_payload) + + firestore_order_data["razorpay_order_id"] = razorpay_order['id'] + + new_order_ref.set(firestore_order_data) + + return JSONResponse( + status_code=200, + content={ + "id": razorpay_order['id'], + "amount": razorpay_order['amount'], + "currency": razorpay_order['currency'], + "key_id": os.environ.get("RAZORPAY_KEY_ID"), + "internal_order_id": internal_order_id, + "message": "Item reserved. Please complete payment in 5 minutes." + } + ) + + except Exception as e: + return JSONResponse(status_code=409, content={"message": str(e)}) + + except Exception as e: + return JSONResponse(status_code=500, content={"message": str(e)}) + +async def get_discounted_feed(id_token: str): + try: + user_data, _ = await get_user_details(id_token) + if not user_data: + return JSONResponse(status_code=401, content={"message": "Unauthorized"}) + + college_id = user_data.get("college_id") + now = datetime.now() + + resale_ref = ( + db.collection("resale_items") + .where("college_id", "==", college_id) + .where("status", "in", ["AVAILABLE", "RESERVED"]) + .order_by("created_at", direction=firestore.Query.DESCENDING) + ) + + docs = resale_ref.stream() + + feed_items = [] + for doc in docs: + data = doc.to_dict() + + if data.get("status") == "RESERVED": + reserved_at = data.get("reserved_at") + if reserved_at and (now - reserved_at.replace(tzinfo=None)) < timedelta(minutes=5): + continue + + data["resale_id"] = doc.id + data = serialize_firestore_data(data) + feed_items.append(data) + + return JSONResponse(status_code=200, content=feed_items) + + except Exception as e: + return JSONResponse(status_code=500, content={"message": str(e)}) diff --git a/app/webhook.py b/app/webhook.py index 1bd359a..fec0224 100644 --- a/app/webhook.py +++ b/app/webhook.py @@ -38,6 +38,9 @@ async def razorpay_webhook(request: Request): internal_order_id = notes.get('internal_order_id') payment_id = payment.get('id') + is_resale = notes.get('type') == 'RESALE' + resale_item_id = notes.get('resale_item_id') + if internal_order_id: order_ref = db.collection('orders').document(internal_order_id) @@ -67,6 +70,15 @@ def update_in_transaction(transaction, order_ref): }) print(f"✅ SUCCESS: Generated Pickup Code {pickup_code} for Order {internal_order_id}") + if is_resale and resale_item_id: + resale_ref = db.collection("resale_items").document(resale_item_id) + transaction.update(resale_ref, { + "status": "SOLD", + "sold_to_order_id": internal_order_id, + "sold_at": firestore.SERVER_TIMESTAMP + }) + print(f"✅ SUCCESS: Marked Resale Item {resale_item_id} as SOLD") + try: update_in_transaction(transaction, order_ref) except Exception as e: