This commit is contained in:
Iliyan Angelov
2025-11-19 12:27:01 +02:00
parent 2043ac897c
commit 34b4c969d4
469 changed files with 26870 additions and 8329 deletions

View File

@@ -1,6 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response
from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response, Request, UploadFile, File
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from pathlib import Path
import aiofiles
import uuid
import os
from ..config.database import get_db
from ..services.auth_service import auth_service
@@ -12,7 +16,11 @@ from ..schemas.auth import (
ResetPasswordRequest,
AuthResponse,
TokenResponse,
MessageResponse
MessageResponse,
MFAInitResponse,
EnableMFARequest,
VerifyMFARequest,
MFAStatusResponse
)
from ..middleware.auth import get_current_user
from ..models.user import User
@@ -20,6 +28,22 @@ from ..models.user import User
router = APIRouter(prefix="/auth", tags=["auth"])
def get_base_url(request: Request) -> str:
"""Get base URL for image normalization"""
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}"
def normalize_image_url(image_url: str, base_url: str) -> str:
"""Normalize image URL to absolute URL"""
if not image_url:
return image_url
if image_url.startswith('http://') or image_url.startswith('https://'):
return image_url
if image_url.startswith('/'):
return f"{base_url}{image_url}"
return f"{base_url}/{image_url}"
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequest,
@@ -79,9 +103,18 @@ async def login(
db=db,
email=request.email,
password=request.password,
remember_me=request.rememberMe or False
remember_me=request.rememberMe or False,
mfa_token=request.mfaToken
)
# Check if MFA is required
if result.get("requires_mfa"):
return {
"status": "success",
"requires_mfa": True,
"user_id": result["user_id"]
}
# Set refresh token as HttpOnly cookie
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
response.set_cookie(
@@ -104,7 +137,7 @@ async def login(
}
except ValueError as e:
error_message = str(e)
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message else status.HTTP_400_BAD_REQUEST
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message or "Invalid MFA token" in error_message else status.HTTP_400_BAD_REQUEST
return JSONResponse(
status_code=status_code,
content={
@@ -260,3 +293,229 @@ async def reset_password(
detail=str(e)
)
# MFA Routes
from ..services.mfa_service import mfa_service
from ..config.settings import settings
@router.get("/mfa/init")
async def init_mfa(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Initialize MFA setup - generate secret and QR code"""
try:
if current_user.mfa_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MFA is already enabled"
)
secret = mfa_service.generate_secret()
app_name = getattr(settings, 'APP_NAME', 'Hotel Booking')
qr_code = mfa_service.generate_qr_code(secret, current_user.email, app_name)
return {
"status": "success",
"data": {
"secret": secret,
"qr_code": qr_code
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error initializing MFA: {str(e)}"
)
@router.post("/mfa/enable")
async def enable_mfa(
request: EnableMFARequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Enable MFA after verifying token"""
try:
success, backup_codes = mfa_service.enable_mfa(
db=db,
user_id=current_user.id,
secret=request.secret,
verification_token=request.verification_token
)
return {
"status": "success",
"message": "MFA enabled successfully",
"data": {
"backup_codes": backup_codes
}
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error enabling MFA: {str(e)}"
)
@router.post("/mfa/disable")
async def disable_mfa(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Disable MFA"""
try:
mfa_service.disable_mfa(db=db, user_id=current_user.id)
return {
"status": "success",
"message": "MFA disabled successfully"
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error disabling MFA: {str(e)}"
)
@router.get("/mfa/status", response_model=MFAStatusResponse)
async def get_mfa_status(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get MFA status for current user"""
try:
status_data = mfa_service.get_mfa_status(db=db, user_id=current_user.id)
return status_data
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting MFA status: {str(e)}"
)
@router.post("/mfa/regenerate-backup-codes")
async def regenerate_backup_codes(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Regenerate backup codes for MFA"""
try:
backup_codes = mfa_service.regenerate_backup_codes(db=db, user_id=current_user.id)
return {
"status": "success",
"message": "Backup codes regenerated successfully",
"data": {
"backup_codes": backup_codes
}
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error regenerating backup codes: {str(e)}"
)
@router.post("/avatar/upload")
async def upload_avatar(
request: Request,
image: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Upload user avatar"""
try:
# Validate file type
if not image.content_type or not image.content_type.startswith('image/'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must be an image"
)
# Validate file size (max 2MB)
content = await image.read()
if len(content) > 2 * 1024 * 1024: # 2MB
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Avatar file size must be less than 2MB"
)
# Create uploads directory
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "avatars"
upload_dir.mkdir(parents=True, exist_ok=True)
# Delete old avatar if exists
if current_user.avatar:
old_avatar_path = Path(__file__).parent.parent.parent / current_user.avatar.lstrip('/')
if old_avatar_path.exists() and old_avatar_path.is_file():
try:
old_avatar_path.unlink()
except Exception:
pass # Ignore deletion errors
# Generate filename
ext = Path(image.filename).suffix or '.png'
filename = f"avatar-{current_user.id}-{uuid.uuid4()}{ext}"
file_path = upload_dir / filename
# Save file
async with aiofiles.open(file_path, 'wb') as f:
await f.write(content)
# Update user avatar
image_url = f"/uploads/avatars/{filename}"
current_user.avatar = image_url
db.commit()
db.refresh(current_user)
# Return the image URL
base_url = get_base_url(request)
full_url = normalize_image_url(image_url, base_url)
return {
"status": "success",
"message": "Avatar uploaded successfully",
"data": {
"avatar_url": image_url,
"full_url": full_url,
"user": {
"id": current_user.id,
"name": current_user.full_name,
"email": current_user.email,
"phone": current_user.phone,
"avatar": image_url,
"role": current_user.role.name if current_user.role else "customer"
}
}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error uploading avatar: {str(e)}"
)

View File

@@ -202,6 +202,16 @@ async def create_booking(
):
"""Create new booking"""
try:
import logging
logger = logging.getLogger(__name__)
# Validate that booking_data is a dict
if not isinstance(booking_data, dict):
logger.error(f"Invalid booking_data type: {type(booking_data)}, value: {booking_data}")
raise HTTPException(status_code=400, detail="Invalid request body. Expected JSON object.")
logger.info(f"Received booking request from user {current_user.id}: {booking_data}")
room_id = booking_data.get("room_id")
check_in_date = booking_data.get("check_in_date")
check_out_date = booking_data.get("check_out_date")
@@ -210,8 +220,21 @@ async def create_booking(
notes = booking_data.get("notes")
payment_method = booking_data.get("payment_method", "cash")
if not all([room_id, check_in_date, check_out_date, total_price]):
raise HTTPException(status_code=400, detail="Missing required booking fields")
# Detailed validation with specific error messages
missing_fields = []
if not room_id:
missing_fields.append("room_id")
if not check_in_date:
missing_fields.append("check_in_date")
if not check_out_date:
missing_fields.append("check_out_date")
if total_price is None:
missing_fields.append("total_price")
if missing_fields:
error_msg = f"Missing required booking fields: {', '.join(missing_fields)}"
logger.error(error_msg)
raise HTTPException(status_code=400, detail=error_msg)
# Check if room exists
room = db.query(Room).filter(Room.id == room_id).first()
@@ -250,15 +273,15 @@ async def create_booking(
booking_number = generate_booking_number()
# Determine if deposit is required
# Cash requires deposit, Stripe doesn't require deposit (full payment or deposit handled via payment flow)
# Cash requires deposit, Stripe and PayPal don't require deposit (full payment or deposit handled via payment flow)
requires_deposit = payment_method == "cash"
deposit_percentage = 20 if requires_deposit else 0
deposit_amount = (float(total_price) * deposit_percentage) / 100 if requires_deposit else 0
# For Stripe, booking can be confirmed immediately after payment
# For Stripe and PayPal, booking can be confirmed immediately after payment
initial_status = BookingStatus.pending
if payment_method == "stripe":
# Will be confirmed after successful Stripe payment
if payment_method in ["stripe", "paypal"]:
# Will be confirmed after successful payment
initial_status = BookingStatus.pending
# Create booking
@@ -279,19 +302,32 @@ async def create_booking(
db.add(booking)
db.flush()
# Create payment record if Stripe payment method is selected
if payment_method == "stripe":
# Create payment record if Stripe or PayPal payment method is selected
if payment_method in ["stripe", "paypal"]:
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
if payment_method == "stripe":
payment_method_enum = PaymentMethod.stripe
elif payment_method == "paypal":
payment_method_enum = PaymentMethod.paypal
else:
# This shouldn't happen, but just in case
logger.warning(f"Unexpected payment_method: {payment_method}, defaulting to stripe")
payment_method_enum = PaymentMethod.stripe
logger.info(f"Creating payment for booking {booking.id} with payment_method: {payment_method} -> enum: {payment_method_enum.value}")
payment = Payment(
booking_id=booking.id,
amount=total_price,
payment_method=PaymentMethod.stripe,
payment_method=payment_method_enum,
payment_type=PaymentType.full,
payment_status=PaymentStatus.pending,
payment_date=None,
)
db.add(payment)
db.flush()
logger.info(f"Payment created: ID={payment.id}, method={payment.payment_method.value if hasattr(payment.payment_method, 'value') else payment.payment_method}")
# Create deposit payment if required (for cash method)
# Note: For cash payments, deposit is paid on arrival, so we don't create a pending payment record
@@ -301,7 +337,7 @@ async def create_booking(
services = booking_data.get("services", [])
if services:
from ..models.service import Service
from ..models.service_usage import ServiceUsage
# ServiceUsage is already imported at the top of the file
for service_item in services:
service_id = service_item.get("service_id")
@@ -354,8 +390,10 @@ async def create_booking(
except Exception as e:
# Log error but don't fail booking creation if invoice creation fails
import logging
import traceback
logger = logging.getLogger(__name__)
logger.error(f"Failed to create invoice for booking {booking.id}: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
# Fetch with relations for proper serialization (eager load payments and service_usages)
from sqlalchemy.orm import joinedload, selectinload
@@ -369,12 +407,25 @@ async def create_booking(
payment_status_from_payments = "unpaid"
if booking.payments:
latest_payment = sorted(booking.payments, key=lambda p: p.created_at, reverse=True)[0]
payment_method_from_payments = latest_payment.payment_method.value if isinstance(latest_payment.payment_method, PaymentMethod) else latest_payment.payment_method
# Safely extract payment method value
if isinstance(latest_payment.payment_method, PaymentMethod):
payment_method_from_payments = latest_payment.payment_method.value
elif hasattr(latest_payment.payment_method, 'value'):
payment_method_from_payments = latest_payment.payment_method.value
else:
payment_method_from_payments = str(latest_payment.payment_method)
logger.info(f"Booking {booking.id} - Latest payment method: {payment_method_from_payments}, raw: {latest_payment.payment_method}")
if latest_payment.payment_status == PaymentStatus.completed:
payment_status_from_payments = "paid"
elif latest_payment.payment_status == PaymentStatus.refunded:
payment_status_from_payments = "refunded"
# Use payment_method from payments if available, otherwise fall back to request payment_method
final_payment_method = payment_method_from_payments if payment_method_from_payments else payment_method
logger.info(f"Booking {booking.id} - Final payment_method: {final_payment_method} (from_payments: {payment_method_from_payments}, request: {payment_method})")
# Serialize booking properly
booking_dict = {
"id": booking.id,
@@ -386,7 +437,7 @@ async def create_booking(
"guest_count": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"payment_method": payment_method_from_payments or payment_method,
"payment_method": final_payment_method,
"payment_status": payment_status_from_payments,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
@@ -408,7 +459,7 @@ async def create_booking(
"id": p.id,
"booking_id": p.booking_id,
"amount": float(p.amount) if p.amount else 0.0,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else p.payment_method,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else (p.payment_method.value if hasattr(p.payment_method, 'value') else str(p.payment_method)),
"payment_type": p.payment_type.value if isinstance(p.payment_type, PaymentType) else p.payment_type,
"deposit_percentage": p.deposit_percentage,
"payment_status": p.payment_status.value if isinstance(p.payment_status, PaymentStatus) else p.payment_status,
@@ -495,6 +546,11 @@ async def create_booking(
except HTTPException:
raise
except Exception as e:
import logging
import traceback
logger = logging.getLogger(__name__)
logger.error(f"Error creating booking (payment_method: {payment_method}): {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@@ -530,17 +586,33 @@ async def get_booking_by_id(
# Determine payment_method and payment_status from payments
# Get latest payment efficiently (already loaded via joinedload)
payment_method = None
import logging
logger = logging.getLogger(__name__)
payment_method_from_payments = None
payment_status = "unpaid"
if booking.payments:
# Find latest payment (payments are already loaded, so this is fast)
latest_payment = max(booking.payments, key=lambda p: p.created_at if p.created_at else datetime.min)
payment_method = latest_payment.payment_method.value if isinstance(latest_payment.payment_method, PaymentMethod) else latest_payment.payment_method
# Safely extract payment method value
if isinstance(latest_payment.payment_method, PaymentMethod):
payment_method_from_payments = latest_payment.payment_method.value
elif hasattr(latest_payment.payment_method, 'value'):
payment_method_from_payments = latest_payment.payment_method.value
else:
payment_method_from_payments = str(latest_payment.payment_method)
logger.info(f"Get booking {id} - Latest payment method: {payment_method_from_payments}, raw: {latest_payment.payment_method}")
if latest_payment.payment_status == PaymentStatus.completed:
payment_status = "paid"
elif latest_payment.payment_status == PaymentStatus.refunded:
payment_status = "refunded"
# Use payment_method from payments, fallback to "cash" if no payments
final_payment_method = payment_method_from_payments if payment_method_from_payments else "cash"
logger.info(f"Get booking {id} - Final payment_method: {final_payment_method}")
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
@@ -551,7 +623,7 @@ async def get_booking_by_id(
"guest_count": booking.num_guests, # Frontend expects guest_count
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"payment_method": payment_method or "cash",
"payment_method": final_payment_method,
"payment_status": payment_status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
@@ -605,7 +677,7 @@ async def get_booking_by_id(
{
"id": p.id,
"amount": float(p.amount) if p.amount else 0.0,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else p.payment_method,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else (p.payment_method.value if hasattr(p.payment_method, 'value') else str(p.payment_method)),
"payment_status": p.payment_status.value if isinstance(p.payment_status, PaymentStatus) else p.payment_status,
}
for p in booking.payments

View File

@@ -13,6 +13,7 @@ from ..models.booking import Booking, BookingStatus
from ..utils.mailer import send_email
from ..utils.email_templates import payment_confirmation_email_template
from ..services.stripe_service import StripeService
from ..services.paypal_service import PayPalService
router = APIRouter(prefix="/payments", tags=["payments"])
@@ -588,3 +589,187 @@ async def stripe_webhook(
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/paypal/create-order")
async def create_paypal_order(
order_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create a PayPal order"""
try:
# Check if PayPal is configured
from ..services.paypal_service import get_paypal_client_id, get_paypal_client_secret
client_id = get_paypal_client_id(db)
if not client_id:
client_id = settings.PAYPAL_CLIENT_ID
client_secret = get_paypal_client_secret(db)
if not client_secret:
client_secret = settings.PAYPAL_CLIENT_SECRET
if not client_id or not client_secret:
raise HTTPException(
status_code=500,
detail="PayPal is not configured. Please configure PayPal settings in Admin Panel or set PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET environment variables."
)
booking_id = order_data.get("booking_id")
amount = float(order_data.get("amount", 0))
currency = order_data.get("currency", "USD")
if not booking_id or amount <= 0:
raise HTTPException(
status_code=400,
detail="booking_id and amount are required"
)
# Validate amount
if amount > 100000:
raise HTTPException(
status_code=400,
detail=f"Amount ${amount:,.2f} exceeds PayPal's maximum of $100,000. Please contact support for large payments."
)
# Verify booking exists and user has access
booking = db.query(Booking).filter(Booking.id == booking_id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
if current_user.role_id != 1 and booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
# Get return URLs from request or use defaults
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
return_url = order_data.get("return_url", f"{client_url}/payment/paypal/return")
cancel_url = order_data.get("cancel_url", f"{client_url}/payment/paypal/cancel")
# Create PayPal order
order = PayPalService.create_order(
amount=amount,
currency=currency,
metadata={
"booking_id": str(booking_id),
"booking_number": booking.booking_number,
"user_id": str(current_user.id),
"description": f"Hotel Booking Payment - {booking.booking_number}",
"return_url": return_url,
"cancel_url": cancel_url,
},
db=db
)
if not order.get("approval_url"):
raise HTTPException(
status_code=500,
detail="Failed to create PayPal order. Approval URL is missing."
)
return {
"status": "success",
"message": "PayPal order created successfully",
"data": {
"order_id": order["id"],
"approval_url": order["approval_url"],
"status": order["status"],
}
}
except HTTPException:
raise
except ValueError as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"PayPal order creation error: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Unexpected error creating PayPal order: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/paypal/capture")
async def capture_paypal_payment(
payment_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Capture a PayPal payment"""
try:
order_id = payment_data.get("order_id")
booking_id = payment_data.get("booking_id")
if not order_id:
raise HTTPException(
status_code=400,
detail="order_id is required"
)
# Confirm payment (this commits the transaction internally)
payment = PayPalService.confirm_payment(
order_id=order_id,
db=db,
booking_id=booking_id
)
# Ensure the transaction is committed
try:
db.commit()
except Exception:
pass
# Get fresh booking from database
booking = db.query(Booking).filter(Booking.id == payment["booking_id"]).first()
if booking:
db.refresh(booking)
# Send payment confirmation email (non-blocking)
if booking and booking.user:
try:
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
email_html = payment_confirmation_email_template(
booking_number=booking.booking_number,
guest_name=booking.user.full_name,
amount=payment["amount"],
payment_method="paypal",
transaction_id=payment["transaction_id"],
client_url=client_url
)
await send_email(
to=booking.user.email,
subject=f"Payment Confirmed - {booking.booking_number}",
html=email_html
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Failed to send payment confirmation email: {e}")
return {
"status": "success",
"message": "Payment confirmed successfully",
"data": {
"payment": payment,
"booking": {
"id": booking.id if booking else None,
"booking_number": booking.booking_number if booking else None,
"status": booking.status.value if booking else None,
}
}
}
except HTTPException:
db.rollback()
raise
except ValueError as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"PayPal payment confirmation error: {str(e)}")
db.rollback()
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Unexpected error confirming PayPal payment: {str(e)}", exc_info=True)
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -122,17 +122,80 @@ async def search_available_rooms(
request: Request,
from_date: str = Query(..., alias="from"),
to_date: str = Query(..., alias="to"),
roomId: Optional[int] = Query(None, alias="roomId"),
type: Optional[str] = Query(None),
capacity: Optional[int] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(12, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Search for available rooms"""
"""Search for available rooms or check specific room availability"""
try:
check_in = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
check_out = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
# Parse dates - handle both date-only and datetime formats
try:
if 'T' in from_date or 'Z' in from_date or '+' in from_date:
check_in = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
else:
check_in = datetime.strptime(from_date, '%Y-%m-%d')
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid from date format: {from_date}")
try:
if 'T' in to_date or 'Z' in to_date or '+' in to_date:
check_out = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
else:
check_out = datetime.strptime(to_date, '%Y-%m-%d')
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid to date format: {to_date}")
# If checking a specific room, handle it differently
if roomId:
# Check if room exists
room = db.query(Room).filter(Room.id == roomId).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Check if room is available
if room.status != RoomStatus.available:
return {
"status": "success",
"data": {
"available": False,
"message": "Room is not available",
"room_id": roomId
}
}
# Check for overlapping bookings
overlapping = db.query(Booking).filter(
and_(
Booking.room_id == roomId,
Booking.status != BookingStatus.cancelled,
Booking.check_in_date < check_out,
Booking.check_out_date > check_in
)
).first()
if overlapping:
return {
"status": "success",
"data": {
"available": False,
"message": "Room is already booked for the selected dates",
"room_id": roomId
}
}
return {
"status": "success",
"data": {
"available": True,
"message": "Room is available",
"room_id": roomId
}
}
# Original search functionality
if check_in >= check_out:
raise HTTPException(
status_code=400,

View File

@@ -323,6 +323,162 @@ async def update_stripe_settings(
raise HTTPException(status_code=500, detail=str(e))
@router.get("/paypal")
async def get_paypal_settings(
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get PayPal payment settings (Admin only)"""
try:
client_id_setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_client_id"
).first()
client_secret_setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_client_secret"
).first()
mode_setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_mode"
).first()
# Mask secret for security (only show last 4 characters)
def mask_key(key_value: str) -> str:
if not key_value or len(key_value) < 4:
return ""
return "*" * (len(key_value) - 4) + key_value[-4:]
result = {
"paypal_client_id": "",
"paypal_client_secret": "",
"paypal_mode": "sandbox",
"paypal_client_secret_masked": "",
"has_client_id": False,
"has_client_secret": False,
}
if client_id_setting:
result["paypal_client_id"] = client_id_setting.value
result["has_client_id"] = bool(client_id_setting.value)
result["updated_at"] = client_id_setting.updated_at.isoformat() if client_id_setting.updated_at else None
result["updated_by"] = client_id_setting.updated_by.full_name if client_id_setting.updated_by else None
if client_secret_setting:
result["paypal_client_secret"] = client_secret_setting.value
result["paypal_client_secret_masked"] = mask_key(client_secret_setting.value)
result["has_client_secret"] = bool(client_secret_setting.value)
if mode_setting:
result["paypal_mode"] = mode_setting.value or "sandbox"
return {
"status": "success",
"data": result
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/paypal")
async def update_paypal_settings(
paypal_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update PayPal payment settings (Admin only)"""
try:
client_id = paypal_data.get("paypal_client_id", "").strip()
client_secret = paypal_data.get("paypal_client_secret", "").strip()
mode = paypal_data.get("paypal_mode", "sandbox").strip().lower()
# Validate mode
if mode and mode not in ["sandbox", "live"]:
raise HTTPException(
status_code=400,
detail="Invalid PayPal mode. Must be 'sandbox' or 'live'"
)
# Update or create client ID setting
if client_id:
setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_client_id"
).first()
if setting:
setting.value = client_id
setting.updated_by_id = current_user.id
else:
setting = SystemSettings(
key="paypal_client_id",
value=client_id,
description="PayPal client ID for processing payments",
updated_by_id=current_user.id
)
db.add(setting)
# Update or create client secret setting
if client_secret:
setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_client_secret"
).first()
if setting:
setting.value = client_secret
setting.updated_by_id = current_user.id
else:
setting = SystemSettings(
key="paypal_client_secret",
value=client_secret,
description="PayPal client secret for processing payments",
updated_by_id=current_user.id
)
db.add(setting)
# Update or create mode setting
if mode:
setting = db.query(SystemSettings).filter(
SystemSettings.key == "paypal_mode"
).first()
if setting:
setting.value = mode
setting.updated_by_id = current_user.id
else:
setting = SystemSettings(
key="paypal_mode",
value=mode,
description="PayPal mode: sandbox or live",
updated_by_id=current_user.id
)
db.add(setting)
db.commit()
# Return masked values
def mask_key(key_value: str) -> str:
if not key_value or len(key_value) < 4:
return ""
return "*" * (len(key_value) - 4) + key_value[-4:]
return {
"status": "success",
"message": "PayPal settings updated successfully",
"data": {
"paypal_client_id": client_id if client_id else "",
"paypal_client_secret": client_secret if client_secret else "",
"paypal_mode": mode,
"paypal_client_secret_masked": mask_key(client_secret) if client_secret else "",
"has_client_id": bool(client_id),
"has_client_secret": bool(client_secret),
}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/smtp")
async def get_smtp_settings(
current_user: User = Depends(authorize_roles("admin")),