from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session, joinedload from typing import Optional from datetime import datetime import random from ..config.database import get_db from ..middleware.auth import get_current_user from ..models.user import User from ..models.service import Service from ..models.service_booking import ( ServiceBooking, ServiceBookingItem, ServicePayment, ServiceBookingStatus, ServicePaymentStatus, ServicePaymentMethod ) from ..services.stripe_service import StripeService, get_stripe_secret_key, get_stripe_publishable_key from ..config.settings import settings router = APIRouter(prefix="/service-bookings", tags=["service-bookings"]) def generate_service_booking_number() -> str: prefix = "SB" timestamp = datetime.utcnow().strftime("%Y%m%d") random_suffix = random.randint(1000, 9999) return f"{prefix}{timestamp}{random_suffix}" @router.post("/") async def create_service_booking( booking_data: dict, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): try: services = booking_data.get("services", []) total_amount = float(booking_data.get("total_amount", 0)) notes = booking_data.get("notes") if not services or len(services) == 0: raise HTTPException(status_code=400, detail="At least one service is required") if total_amount <= 0: raise HTTPException(status_code=400, detail="Total amount must be greater than 0") calculated_total = 0 service_items_data = [] for service_item in services: service_id = service_item.get("service_id") quantity = service_item.get("quantity", 1) if not service_id: raise HTTPException(status_code=400, detail="Service ID is required for each item") service = db.query(Service).filter(Service.id == service_id).first() if not service: raise HTTPException(status_code=404, detail=f"Service with ID {service_id} not found") if not service.is_active: raise HTTPException(status_code=400, detail=f"Service {service.name} is not active") unit_price = float(service.price) item_total = unit_price * quantity calculated_total += item_total service_items_data.append({ "service": service, "quantity": quantity, "unit_price": unit_price, "total_price": item_total }) if abs(calculated_total - total_amount) > 0.01: raise HTTPException( status_code=400, detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}" ) booking_number = generate_service_booking_number() service_booking = ServiceBooking( booking_number=booking_number, user_id=current_user.id, total_amount=total_amount, status=ServiceBookingStatus.pending, notes=notes ) db.add(service_booking) db.flush() for item_data in service_items_data: booking_item = ServiceBookingItem( service_booking_id=service_booking.id, service_id=item_data["service"].id, quantity=item_data["quantity"], unit_price=item_data["unit_price"], total_price=item_data["total_price"] ) db.add(booking_item) db.commit() db.refresh(service_booking) service_booking = db.query(ServiceBooking).options( joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service) ).filter(ServiceBooking.id == service_booking.id).first() booking_dict = { "id": service_booking.id, "booking_number": service_booking.booking_number, "user_id": service_booking.user_id, "total_amount": float(service_booking.total_amount), "status": service_booking.status.value, "notes": service_booking.notes, "created_at": service_booking.created_at.isoformat() if service_booking.created_at else None, "service_items": [ { "id": item.id, "service_id": item.service_id, "quantity": item.quantity, "unit_price": float(item.unit_price), "total_price": float(item.total_price), "service": { "id": item.service.id, "name": item.service.name, "description": item.service.description, "price": float(item.service.price), } } for item in service_booking.service_items ] } return { "status": "success", "message": "Service booking created successfully", "data": {"service_booking": booking_dict} } except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=str(e)) @router.get("/me") async def get_my_service_bookings( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): try: bookings = db.query(ServiceBooking).options( joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service) ).filter(ServiceBooking.user_id == current_user.id).order_by(ServiceBooking.created_at.desc()).all() result = [] for booking in bookings: booking_dict = { "id": booking.id, "booking_number": booking.booking_number, "total_amount": float(booking.total_amount), "status": booking.status.value, "notes": booking.notes, "created_at": booking.created_at.isoformat() if booking.created_at else None, "service_items": [ { "id": item.id, "service_id": item.service_id, "quantity": item.quantity, "unit_price": float(item.unit_price), "total_price": float(item.total_price), "service": { "id": item.service.id, "name": item.service.name, "description": item.service.description, "price": float(item.service.price), } } for item in booking.service_items ] } result.append(booking_dict) return { "status": "success", "data": {"service_bookings": result} } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/{id}") async def get_service_booking_by_id( id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): try: booking = db.query(ServiceBooking).options( joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service) ).filter(ServiceBooking.id == id).first() if not booking: raise HTTPException(status_code=404, detail="Service booking not found") if booking.user_id != current_user.id and current_user.role_id != 1: raise HTTPException(status_code=403, detail="Forbidden") booking_dict = { "id": booking.id, "booking_number": booking.booking_number, "user_id": booking.user_id, "total_amount": float(booking.total_amount), "status": booking.status.value, "notes": booking.notes, "created_at": booking.created_at.isoformat() if booking.created_at else None, "service_items": [ { "id": item.id, "service_id": item.service_id, "quantity": item.quantity, "unit_price": float(item.unit_price), "total_price": float(item.total_price), "service": { "id": item.service.id, "name": item.service.name, "description": item.service.description, "price": float(item.service.price), } } for item in booking.service_items ] } return { "status": "success", "data": {"service_booking": booking_dict} } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{id}/payment/stripe/create-intent") async def create_service_stripe_payment_intent( id: int, intent_data: dict, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): try: secret_key = get_stripe_secret_key(db) if not secret_key: secret_key = settings.STRIPE_SECRET_KEY if not secret_key: raise HTTPException( status_code=500, detail="Stripe is not configured. Please configure Stripe settings in Admin Panel." ) amount = float(intent_data.get("amount", 0)) currency = intent_data.get("currency", "usd") if amount <= 0: raise HTTPException(status_code=400, detail="Amount must be greater than 0") booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first() if not booking: raise HTTPException(status_code=404, detail="Service booking not found") if booking.user_id != current_user.id and current_user.role_id != 1: raise HTTPException(status_code=403, detail="Forbidden") if abs(float(booking.total_amount) - amount) > 0.01: raise HTTPException( status_code=400, detail=f"Amount mismatch. Booking total: {booking.total_amount}, Provided: {amount}" ) intent = StripeService.create_payment_intent( amount=amount, currency=currency, description=f"Service Booking #{booking.id}", db=db ) publishable_key = get_stripe_publishable_key(db) if not publishable_key: publishable_key = settings.STRIPE_PUBLISHABLE_KEY if not publishable_key: raise HTTPException( status_code=500, detail="Stripe publishable key is not configured." ) return { "status": "success", "data": { "client_secret": intent["client_secret"], "payment_intent_id": intent["id"], "publishable_key": publishable_key } } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{id}/payment/stripe/confirm") async def confirm_service_stripe_payment( id: int, payment_data: dict, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): try: payment_intent_id = payment_data.get("payment_intent_id") if not payment_intent_id: raise HTTPException(status_code=400, detail="payment_intent_id is required") booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first() if not booking: raise HTTPException(status_code=404, detail="Service booking not found") if booking.user_id != current_user.id and current_user.role_id != 1: raise HTTPException(status_code=403, detail="Forbidden") intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db) if intent_data["status"] != "succeeded": raise HTTPException( status_code=400, detail=f"Payment intent status is {intent_data['status']}, expected 'succeeded'" ) amount_paid = intent_data["amount"] / 100 if abs(float(booking.total_amount) - amount_paid) > 0.01: raise HTTPException( status_code=400, detail="Payment amount does not match booking total" ) payment = ServicePayment( service_booking_id=booking.id, amount=booking.total_amount, payment_method=ServicePaymentMethod.stripe, payment_status=ServicePaymentStatus.completed, transaction_id=payment_intent_id, payment_date=datetime.utcnow(), notes=f"Stripe payment - Intent: {payment_intent_id}" ) db.add(payment) booking.status = ServiceBookingStatus.confirmed db.commit() db.refresh(payment) db.refresh(booking) return { "status": "success", "message": "Payment confirmed successfully", "data": { "payment": { "id": payment.id, "amount": float(payment.amount), "payment_method": payment.payment_method.value, "payment_status": payment.payment_status.value, "transaction_id": payment.transaction_id, }, "service_booking": { "id": booking.id, "booking_number": booking.booking_number, "status": booking.status.value, } } } except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=str(e))