updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
Backend/src/routes/__pycache__/invoice_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/invoice_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -196,7 +196,8 @@ async def update_profile(
|
||||
email=profile_data.get("email"),
|
||||
phone_number=profile_data.get("phone_number"),
|
||||
password=profile_data.get("password"),
|
||||
current_password=profile_data.get("currentPassword")
|
||||
current_password=profile_data.get("currentPassword"),
|
||||
currency=profile_data.get("currency")
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
from sqlalchemy import and_, or_
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
@@ -11,9 +11,11 @@ from ..config.settings import settings
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.room import Room
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ..services.room_service import normalize_images, get_base_url
|
||||
from fastapi import Request
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import (
|
||||
booking_confirmation_email_template,
|
||||
@@ -129,6 +131,7 @@ async def get_all_bookings(
|
||||
|
||||
@router.get("/me")
|
||||
async def get_my_bookings(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -138,6 +141,7 @@ async def get_my_bookings(
|
||||
Booking.user_id == current_user.id
|
||||
).order_by(Booking.created_at.desc()).all()
|
||||
|
||||
base_url = get_base_url(request)
|
||||
result = []
|
||||
for booking in bookings:
|
||||
booking_dict = {
|
||||
@@ -157,11 +161,25 @@ async def get_my_bookings(
|
||||
|
||||
# Add room info
|
||||
if booking.room and booking.room.room_type:
|
||||
# Normalize room images if they exist
|
||||
room_images = []
|
||||
if booking.room.images:
|
||||
try:
|
||||
room_images = normalize_images(booking.room.images, base_url)
|
||||
except:
|
||||
room_images = []
|
||||
|
||||
booking_dict["room"] = {
|
||||
"id": booking.room.id,
|
||||
"room_number": booking.room.room_number,
|
||||
"floor": booking.room.floor,
|
||||
"images": room_images, # Include room images
|
||||
"room_type": {
|
||||
"id": booking.room.room_type.id,
|
||||
"name": booking.room.room_type.name,
|
||||
"base_price": float(booking.room.room_type.base_price) if booking.room.room_type.base_price else 0.0,
|
||||
"capacity": booking.room.room_type.capacity,
|
||||
"images": room_images, # Also include in room_type for backwards compatibility
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +239,17 @@ 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)
|
||||
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
|
||||
initial_status = BookingStatus.pending
|
||||
if payment_method == "stripe":
|
||||
# Will be confirmed after successful Stripe payment
|
||||
initial_status = BookingStatus.pending
|
||||
|
||||
# Create booking
|
||||
booking = Booking(
|
||||
booking_number=booking_number,
|
||||
@@ -235,7 +260,7 @@ async def create_booking(
|
||||
num_guests=guest_count,
|
||||
total_price=total_price,
|
||||
special_requests=notes,
|
||||
status=BookingStatus.pending,
|
||||
status=initial_status,
|
||||
requires_deposit=requires_deposit,
|
||||
deposit_paid=False,
|
||||
)
|
||||
@@ -243,24 +268,101 @@ async def create_booking(
|
||||
db.add(booking)
|
||||
db.flush()
|
||||
|
||||
# Create deposit payment if required
|
||||
if requires_deposit:
|
||||
# Create payment record if Stripe payment method is selected
|
||||
if payment_method == "stripe":
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
payment = Payment(
|
||||
booking_id=booking.id,
|
||||
amount=deposit_amount,
|
||||
payment_method=PaymentMethod.bank_transfer,
|
||||
payment_type=PaymentType.deposit,
|
||||
deposit_percentage=deposit_percentage,
|
||||
amount=total_price,
|
||||
payment_method=PaymentMethod.stripe,
|
||||
payment_type=PaymentType.full,
|
||||
payment_status=PaymentStatus.pending,
|
||||
notes=f"Deposit payment ({deposit_percentage}%) for booking {booking_number}",
|
||||
payment_date=None,
|
||||
)
|
||||
db.add(payment)
|
||||
db.flush()
|
||||
|
||||
# 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
|
||||
# The payment will be created when the customer pays at check-in
|
||||
|
||||
db.commit()
|
||||
db.refresh(booking)
|
||||
|
||||
# Fetch with relations
|
||||
booking = db.query(Booking).filter(Booking.id == booking.id).first()
|
||||
# Fetch with relations for proper serialization (eager load payments)
|
||||
from sqlalchemy.orm import joinedload
|
||||
booking = db.query(Booking).options(joinedload(Booking.payments)).filter(Booking.id == booking.id).first()
|
||||
|
||||
# Determine payment_method and payment_status from payments
|
||||
payment_method_from_payments = None
|
||||
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
|
||||
if latest_payment.payment_status == PaymentStatus.completed:
|
||||
payment_status_from_payments = "paid"
|
||||
elif latest_payment.payment_status == PaymentStatus.refunded:
|
||||
payment_status_from_payments = "refunded"
|
||||
|
||||
# Serialize booking properly
|
||||
booking_dict = {
|
||||
"id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
"user_id": booking.user_id,
|
||||
"room_id": booking.room_id,
|
||||
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
|
||||
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
|
||||
"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_status": payment_status_from_payments,
|
||||
"deposit_paid": booking.deposit_paid,
|
||||
"requires_deposit": booking.requires_deposit,
|
||||
"notes": booking.special_requests,
|
||||
"guest_info": {
|
||||
"full_name": current_user.full_name,
|
||||
"email": current_user.email,
|
||||
"phone": current_user.phone_number if hasattr(current_user, 'phone_number') else (current_user.phone if hasattr(current_user, 'phone') else ""),
|
||||
},
|
||||
"createdAt": booking.created_at.isoformat() if booking.created_at else None,
|
||||
"updatedAt": booking.updated_at.isoformat() if booking.updated_at else None,
|
||||
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
||||
}
|
||||
|
||||
# Add payments if they exist
|
||||
if booking.payments:
|
||||
booking_dict["payments"] = [
|
||||
{
|
||||
"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_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,
|
||||
"transaction_id": p.transaction_id,
|
||||
"payment_date": p.payment_date.isoformat() if p.payment_date else None,
|
||||
"notes": p.notes,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
}
|
||||
for p in booking.payments
|
||||
]
|
||||
|
||||
# Add room info if available
|
||||
if booking.room:
|
||||
booking_dict["room"] = {
|
||||
"id": booking.room.id,
|
||||
"room_number": booking.room.room_number,
|
||||
"floor": booking.room.floor,
|
||||
}
|
||||
if booking.room.room_type:
|
||||
booking_dict["room"]["room_type"] = {
|
||||
"id": booking.room.room_type.id,
|
||||
"name": booking.room.room_type.name,
|
||||
"base_price": float(booking.room.room_type.base_price) if booking.room.room_type.base_price else 0.0,
|
||||
"capacity": booking.room.room_type.capacity,
|
||||
}
|
||||
|
||||
# Send booking confirmation email (non-blocking)
|
||||
try:
|
||||
@@ -291,7 +393,7 @@ async def create_booking(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"booking": booking},
|
||||
"data": {"booking": booking_dict},
|
||||
"message": f"Booking created. Please pay {deposit_percentage}% deposit to confirm." if requires_deposit else "Booking created successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
@@ -304,12 +406,22 @@ async def create_booking(
|
||||
@router.get("/{id}")
|
||||
async def get_booking_by_id(
|
||||
id: int,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get booking by ID"""
|
||||
try:
|
||||
booking = db.query(Booking).filter(Booking.id == id).first()
|
||||
# Eager load all relationships to avoid N+1 queries
|
||||
# Using selectinload for better performance with multiple relationships
|
||||
booking = db.query(Booking)\
|
||||
.options(
|
||||
selectinload(Booking.payments),
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
)\
|
||||
.filter(Booking.id == id)\
|
||||
.first()
|
||||
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
@@ -318,6 +430,19 @@ async def get_booking_by_id(
|
||||
if current_user.role_id != 1 and booking.user_id != current_user.id: # Not admin
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# Determine payment_method and payment_status from payments
|
||||
# Get latest payment efficiently (already loaded via joinedload)
|
||||
payment_method = 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
|
||||
if latest_payment.payment_status == PaymentStatus.completed:
|
||||
payment_status = "paid"
|
||||
elif latest_payment.payment_status == PaymentStatus.refunded:
|
||||
payment_status = "refunded"
|
||||
|
||||
booking_dict = {
|
||||
"id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
@@ -325,24 +450,56 @@ async def get_booking_by_id(
|
||||
"room_id": booking.room_id,
|
||||
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
|
||||
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
|
||||
"num_guests": booking.num_guests,
|
||||
"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_status": payment_status,
|
||||
"deposit_paid": booking.deposit_paid,
|
||||
"requires_deposit": booking.requires_deposit,
|
||||
"special_requests": booking.special_requests,
|
||||
"notes": booking.special_requests, # Frontend expects notes
|
||||
"guest_info": {
|
||||
"full_name": booking.user.full_name if booking.user else "",
|
||||
"email": booking.user.email if booking.user else "",
|
||||
"phone": booking.user.phone_number if booking.user and hasattr(booking.user, 'phone_number') else (booking.user.phone if booking.user and hasattr(booking.user, 'phone') else ""),
|
||||
} if booking.user else None,
|
||||
"createdAt": booking.created_at.isoformat() if booking.created_at else None,
|
||||
"updatedAt": booking.updated_at.isoformat() if booking.updated_at else None,
|
||||
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
||||
}
|
||||
|
||||
# Add relations
|
||||
# Only get base_url if we need it (room has images)
|
||||
if booking.room and booking.room.images:
|
||||
base_url = get_base_url(request)
|
||||
# Normalize room images if they exist
|
||||
try:
|
||||
room_images = normalize_images(booking.room.images, base_url)
|
||||
except:
|
||||
room_images = []
|
||||
else:
|
||||
room_images = []
|
||||
|
||||
if booking.room:
|
||||
|
||||
booking_dict["room"] = {
|
||||
"id": booking.room.id,
|
||||
"room_number": booking.room.room_number,
|
||||
"floor": booking.room.floor,
|
||||
"status": booking.room.status.value if isinstance(booking.room.status, RoomStatus) else booking.room.status,
|
||||
"images": room_images, # Include room images directly on room object
|
||||
}
|
||||
if booking.room.room_type:
|
||||
# Use room images if room_type doesn't have images (which is typical)
|
||||
# RoomType doesn't have images column, images are stored on Room
|
||||
room_type_images = room_images if room_images else []
|
||||
|
||||
booking_dict["room"]["room_type"] = {
|
||||
"id": booking.room.room_type.id,
|
||||
"name": booking.room.room_type.name,
|
||||
"base_price": float(booking.room.room_type.base_price) if booking.room.room_type.base_price else 0.0,
|
||||
"capacity": booking.room.room_type.capacity,
|
||||
"images": room_type_images,
|
||||
}
|
||||
|
||||
if booking.payments:
|
||||
@@ -385,6 +542,20 @@ async def cancel_booking(
|
||||
if booking.status == BookingStatus.cancelled:
|
||||
raise HTTPException(status_code=400, detail="Booking already cancelled")
|
||||
|
||||
# Prevent cancellation of confirmed bookings
|
||||
if booking.status == BookingStatus.confirmed:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot cancel a confirmed booking. Please contact support for assistance."
|
||||
)
|
||||
|
||||
# Only allow cancellation of pending bookings
|
||||
if booking.status != BookingStatus.pending:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot cancel booking with status: {booking.status.value}. Only pending bookings can be cancelled."
|
||||
)
|
||||
|
||||
booking.status = BookingStatus.cancelled
|
||||
db.commit()
|
||||
|
||||
|
||||
249
Backend/src/routes/invoice_routes.py
Normal file
249
Backend/src/routes/invoice_routes.py
Normal file
@@ -0,0 +1,249 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.invoice import Invoice, InvoiceStatus
|
||||
from ..models.booking import Booking
|
||||
from ..services.invoice_service import InvoiceService
|
||||
|
||||
router = APIRouter(prefix="/invoices", tags=["invoices"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_invoices(
|
||||
booking_id: Optional[int] = Query(None),
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get invoices for current user (or all invoices for admin)"""
|
||||
try:
|
||||
# Admin can see all invoices, users can only see their own
|
||||
user_id = None if current_user.role_id == 1 else current_user.id
|
||||
|
||||
result = InvoiceService.get_invoices(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
booking_id=booking_id,
|
||||
status=status_filter,
|
||||
page=page,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_invoice_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get invoice by ID"""
|
||||
try:
|
||||
invoice = InvoiceService.get_invoice(id, db)
|
||||
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
# Check access: admin can see all, users can only see their own
|
||||
if current_user.role_id != 1 and invoice["user_id"] != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"invoice": invoice}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_invoice(
|
||||
invoice_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new invoice from a booking (Admin/Staff only)"""
|
||||
try:
|
||||
# Only admin/staff can create invoices
|
||||
if current_user.role_id not in [1, 2]:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
booking_id = invoice_data.get("booking_id")
|
||||
if not booking_id:
|
||||
raise HTTPException(status_code=400, detail="booking_id is required")
|
||||
|
||||
# Check if booking exists
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
|
||||
# Create invoice
|
||||
invoice = InvoiceService.create_invoice_from_booking(
|
||||
booking_id=booking_id,
|
||||
db=db,
|
||||
created_by_id=current_user.id,
|
||||
tax_rate=invoice_data.get("tax_rate", 0.0),
|
||||
discount_amount=invoice_data.get("discount_amount", 0.0),
|
||||
due_days=invoice_data.get("due_days", 30),
|
||||
company_name=invoice_data.get("company_name"),
|
||||
company_address=invoice_data.get("company_address"),
|
||||
company_phone=invoice_data.get("company_phone"),
|
||||
company_email=invoice_data.get("company_email"),
|
||||
company_tax_id=invoice_data.get("company_tax_id"),
|
||||
company_logo_url=invoice_data.get("company_logo_url"),
|
||||
customer_tax_id=invoice_data.get("customer_tax_id"),
|
||||
notes=invoice_data.get("notes"),
|
||||
terms_and_conditions=invoice_data.get("terms_and_conditions"),
|
||||
payment_instructions=invoice_data.get("payment_instructions"),
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice created successfully",
|
||||
"data": {"invoice": invoice}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_invoice(
|
||||
id: int,
|
||||
invoice_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update an invoice (Admin/Staff only)"""
|
||||
try:
|
||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
# Update invoice
|
||||
updated_invoice = InvoiceService.update_invoice(
|
||||
invoice_id=id,
|
||||
db=db,
|
||||
updated_by_id=current_user.id,
|
||||
**invoice_data
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice updated successfully",
|
||||
"data": {"invoice": updated_invoice}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{id}/mark-paid")
|
||||
async def mark_invoice_as_paid(
|
||||
id: int,
|
||||
payment_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Mark an invoice as paid (Admin/Staff only)"""
|
||||
try:
|
||||
amount = payment_data.get("amount")
|
||||
|
||||
updated_invoice = InvoiceService.mark_invoice_as_paid(
|
||||
invoice_id=id,
|
||||
db=db,
|
||||
amount=amount,
|
||||
updated_by_id=current_user.id
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice marked as paid successfully",
|
||||
"data": {"invoice": updated_invoice}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_invoice(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete an invoice (Admin only)"""
|
||||
try:
|
||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
db.delete(invoice)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice deleted successfully"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/booking/{booking_id}")
|
||||
async def get_invoices_by_booking(
|
||||
booking_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all invoices for a specific booking"""
|
||||
try:
|
||||
# Check if 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")
|
||||
|
||||
# Check access: admin can see all, users can only see their own bookings
|
||||
if current_user.role_id != 1 and booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
result = InvoiceService.get_invoices(
|
||||
db=db,
|
||||
booking_id=booking_id
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request, Header
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
@@ -12,6 +12,7 @@ from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["payments"])
|
||||
|
||||
@@ -340,3 +341,250 @@ async def update_payment_status(
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/stripe/create-intent")
|
||||
async def create_stripe_payment_intent(
|
||||
intent_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a Stripe payment intent"""
|
||||
try:
|
||||
# Check if Stripe is configured (from database or environment)
|
||||
from ..services.stripe_service import get_stripe_secret_key
|
||||
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 or set STRIPE_SECRET_KEY environment variable."
|
||||
)
|
||||
|
||||
booking_id = intent_data.get("booking_id")
|
||||
amount = float(intent_data.get("amount", 0))
|
||||
currency = intent_data.get("currency", "usd")
|
||||
|
||||
# Log the incoming amount for debugging
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Creating Stripe payment intent - Booking ID: {booking_id}, Amount: ${amount:,.2f}, Currency: {currency}")
|
||||
|
||||
if not booking_id or amount <= 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="booking_id and amount are required"
|
||||
)
|
||||
|
||||
# Validate amount is reasonable (Stripe max is $999,999.99)
|
||||
if amount > 999999.99:
|
||||
logger.error(f"Amount ${amount:,.2f} exceeds Stripe's maximum of $999,999.99")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Amount ${amount:,.2f} exceeds Stripe's maximum of $999,999.99. 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")
|
||||
|
||||
# Create payment intent
|
||||
intent = StripeService.create_payment_intent(
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
metadata={
|
||||
"booking_id": str(booking_id),
|
||||
"booking_number": booking.booking_number,
|
||||
"user_id": str(current_user.id),
|
||||
},
|
||||
db=db
|
||||
)
|
||||
|
||||
# Get publishable key from database or environment
|
||||
from ..services.stripe_service import get_stripe_publishable_key
|
||||
publishable_key = get_stripe_publishable_key(db)
|
||||
if not publishable_key:
|
||||
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
if not publishable_key:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("Stripe publishable key is not configured")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Stripe publishable key is not configured. Please configure it in Admin Panel (Settings > Stripe Settings) or set STRIPE_PUBLISHABLE_KEY environment variable."
|
||||
)
|
||||
|
||||
if not intent.get("client_secret"):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error("Payment intent created but client_secret is missing")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to create payment intent. Client secret is missing."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Payment intent created successfully",
|
||||
"data": {
|
||||
"client_secret": intent["client_secret"],
|
||||
"payment_intent_id": intent["id"],
|
||||
"publishable_key": publishable_key,
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Payment intent 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 payment intent: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/stripe/confirm")
|
||||
async def confirm_stripe_payment(
|
||||
payment_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Confirm a Stripe payment"""
|
||||
try:
|
||||
payment_intent_id = payment_data.get("payment_intent_id")
|
||||
booking_id = payment_data.get("booking_id")
|
||||
|
||||
if not payment_intent_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="payment_intent_id is required"
|
||||
)
|
||||
|
||||
# Confirm payment (this commits the transaction internally)
|
||||
payment = StripeService.confirm_payment(
|
||||
payment_intent_id=payment_intent_id,
|
||||
db=db,
|
||||
booking_id=booking_id
|
||||
)
|
||||
|
||||
# Ensure the transaction is committed before proceeding
|
||||
# The service method already commits, but we ensure it here too
|
||||
try:
|
||||
db.commit()
|
||||
except Exception:
|
||||
# If already committed, this will raise an error, which we can ignore
|
||||
pass
|
||||
|
||||
# Get fresh booking from database to get updated status (after commit)
|
||||
booking = db.query(Booking).filter(Booking.id == payment["booking_id"]).first()
|
||||
if booking:
|
||||
db.refresh(booking)
|
||||
|
||||
# Send payment confirmation email (non-blocking, after commit)
|
||||
# This won't affect the transaction since it's already committed
|
||||
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="stripe",
|
||||
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"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 payment: {str(e)}", exc_info=True)
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/stripe/webhook")
|
||||
async def stripe_webhook(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle Stripe webhook events"""
|
||||
try:
|
||||
# Check if webhook secret is configured (from database or environment)
|
||||
from ..services.stripe_service import get_stripe_webhook_secret
|
||||
webhook_secret = get_stripe_webhook_secret(db)
|
||||
if not webhook_secret:
|
||||
webhook_secret = settings.STRIPE_WEBHOOK_SECRET
|
||||
|
||||
if not webhook_secret:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail={
|
||||
"status": "error",
|
||||
"message": "Stripe webhook secret is not configured. Please configure it in Admin Panel (Settings > Stripe Settings) or set STRIPE_WEBHOOK_SECRET environment variable."
|
||||
}
|
||||
)
|
||||
|
||||
payload = await request.body()
|
||||
signature = request.headers.get("stripe-signature")
|
||||
|
||||
if not signature:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing stripe-signature header"
|
||||
)
|
||||
|
||||
result = StripeService.handle_webhook(
|
||||
payload=payload,
|
||||
signature=signature,
|
||||
db=db
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -197,7 +197,7 @@ async def search_available_rooms(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
@router.get("/id/{id}")
|
||||
async def get_room_by_id(id: int, request: Request, db: Session = Depends(get_db)):
|
||||
"""Get room by ID"""
|
||||
try:
|
||||
@@ -225,9 +225,81 @@ async def get_room_by_id(id: int, request: Request, db: Session = Depends(get_db
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
|
||||
"price": float(room.price) if room.price else 0.0,
|
||||
"price": float(room.price) if room.price is not None and room.price > 0 else None,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"capacity": room.capacity,
|
||||
"room_size": room.room_size,
|
||||
"view": room.view,
|
||||
"amenities": room.amenities,
|
||||
"created_at": room.created_at.isoformat() if room.created_at else None,
|
||||
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
|
||||
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
|
||||
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
|
||||
}
|
||||
|
||||
# Normalize images
|
||||
try:
|
||||
room_dict["images"] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict["images"] = []
|
||||
|
||||
# Add room type
|
||||
if room.room_type:
|
||||
room_dict["room_type"] = {
|
||||
"id": room.room_type.id,
|
||||
"name": room.room_type.name,
|
||||
"description": room.room_type.description,
|
||||
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
|
||||
"capacity": room.room_type.capacity,
|
||||
"amenities": room.room_type.amenities,
|
||||
"images": [] # RoomType doesn't have images column in DB
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"room": room_dict}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{room_number}")
|
||||
async def get_room_by_number(room_number: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""Get room by room number"""
|
||||
try:
|
||||
room = db.query(Room).filter(Room.room_number == room_number).first()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
# Get review stats
|
||||
review_stats = db.query(
|
||||
func.avg(Review.rating).label('average_rating'),
|
||||
func.count(Review.id).label('total_reviews')
|
||||
).filter(
|
||||
and_(
|
||||
Review.room_id == room.id,
|
||||
Review.status == ReviewStatus.approved
|
||||
)
|
||||
).first()
|
||||
|
||||
base_url = get_base_url(request)
|
||||
|
||||
room_dict = {
|
||||
"id": room.id,
|
||||
"room_type_id": room.room_type_id,
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
|
||||
"price": float(room.price) if room.price is not None and room.price > 0 else None,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"capacity": room.capacity,
|
||||
"room_size": room.room_size,
|
||||
"view": room.view,
|
||||
"amenities": room.amenities,
|
||||
"created_at": room.created_at.isoformat() if room.created_at else None,
|
||||
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
|
||||
@@ -266,6 +338,7 @@ async def get_room_by_id(id: int, request: Request, db: Session = Depends(get_db
|
||||
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def create_room(
|
||||
room_data: dict,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -281,6 +354,13 @@ async def create_room(
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Room number already exists")
|
||||
|
||||
# Ensure amenities is always a list
|
||||
amenities_value = room_data.get("amenities", [])
|
||||
if amenities_value is None:
|
||||
amenities_value = []
|
||||
elif not isinstance(amenities_value, list):
|
||||
amenities_value = []
|
||||
|
||||
room = Room(
|
||||
room_type_id=room_data.get("room_type_id"),
|
||||
room_number=room_data.get("room_number"),
|
||||
@@ -288,16 +368,60 @@ async def create_room(
|
||||
status=RoomStatus(room_data.get("status", "available")),
|
||||
featured=room_data.get("featured", False),
|
||||
price=room_data.get("price", room_type.base_price),
|
||||
description=room_data.get("description"),
|
||||
capacity=room_data.get("capacity"),
|
||||
room_size=room_data.get("room_size"),
|
||||
view=room_data.get("view"),
|
||||
amenities=amenities_value,
|
||||
)
|
||||
|
||||
db.add(room)
|
||||
db.commit()
|
||||
db.refresh(room)
|
||||
|
||||
# Get base URL for proper response
|
||||
base_url = get_base_url(request)
|
||||
|
||||
# Serialize room data
|
||||
room_dict = {
|
||||
"id": room.id,
|
||||
"room_type_id": room.room_type_id,
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
|
||||
"price": float(room.price) if room.price is not None and room.price > 0 else None,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"capacity": room.capacity,
|
||||
"room_size": room.room_size,
|
||||
"view": room.view,
|
||||
"amenities": room.amenities if room.amenities else [],
|
||||
"created_at": room.created_at.isoformat() if room.created_at else None,
|
||||
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
|
||||
}
|
||||
|
||||
# Normalize images
|
||||
try:
|
||||
room_dict["images"] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict["images"] = []
|
||||
|
||||
# Add room type info
|
||||
if room.room_type:
|
||||
room_dict["room_type"] = {
|
||||
"id": room.room_type.id,
|
||||
"name": room.room_type.name,
|
||||
"description": room.room_type.description,
|
||||
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
|
||||
"capacity": room.room_type.capacity,
|
||||
"amenities": room.room_type.amenities if room.room_type.amenities else [],
|
||||
"images": []
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Room created successfully",
|
||||
"data": {"room": room}
|
||||
"data": {"room": room_dict}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -310,6 +434,7 @@ async def create_room(
|
||||
async def update_room(
|
||||
id: int,
|
||||
room_data: dict,
|
||||
request: Request,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -337,14 +462,70 @@ async def update_room(
|
||||
room.featured = room_data["featured"]
|
||||
if "price" in room_data:
|
||||
room.price = room_data["price"]
|
||||
if "description" in room_data:
|
||||
room.description = room_data["description"]
|
||||
if "capacity" in room_data:
|
||||
room.capacity = room_data["capacity"]
|
||||
if "room_size" in room_data:
|
||||
room.room_size = room_data["room_size"]
|
||||
if "view" in room_data:
|
||||
room.view = room_data["view"]
|
||||
if "amenities" in room_data:
|
||||
# Ensure amenities is always a list
|
||||
amenities_value = room_data["amenities"]
|
||||
if amenities_value is None:
|
||||
room.amenities = []
|
||||
elif isinstance(amenities_value, list):
|
||||
room.amenities = amenities_value
|
||||
else:
|
||||
room.amenities = []
|
||||
|
||||
db.commit()
|
||||
db.refresh(room)
|
||||
|
||||
# Get base URL for proper response
|
||||
base_url = get_base_url(request)
|
||||
|
||||
# Serialize room data similar to get_room_by_id
|
||||
room_dict = {
|
||||
"id": room.id,
|
||||
"room_type_id": room.room_type_id,
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
|
||||
"price": float(room.price) if room.price is not None and room.price > 0 else None,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"capacity": room.capacity,
|
||||
"room_size": room.room_size,
|
||||
"view": room.view,
|
||||
"amenities": room.amenities if room.amenities else [],
|
||||
"created_at": room.created_at.isoformat() if room.created_at else None,
|
||||
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
|
||||
}
|
||||
|
||||
# Normalize images
|
||||
try:
|
||||
room_dict["images"] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict["images"] = []
|
||||
|
||||
# Add room type info
|
||||
if room.room_type:
|
||||
room_dict["room_type"] = {
|
||||
"id": room.room_type.id,
|
||||
"name": room.room_type.name,
|
||||
"description": room.room_type.description,
|
||||
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
|
||||
"capacity": room.room_type.capacity,
|
||||
"amenities": room.room_type.amenities if room.room_type.amenities else [],
|
||||
"images": []
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Room updated successfully",
|
||||
"data": {"room": room}
|
||||
"data": {"room": room_dict}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -379,6 +560,57 @@ async def delete_room(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/bulk-delete", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def bulk_delete_rooms(
|
||||
room_ids: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Bulk delete rooms (Admin only)"""
|
||||
try:
|
||||
ids = room_ids.get("ids", [])
|
||||
if not ids or not isinstance(ids, list):
|
||||
raise HTTPException(status_code=400, detail="Invalid room IDs provided")
|
||||
|
||||
if len(ids) == 0:
|
||||
raise HTTPException(status_code=400, detail="No room IDs provided")
|
||||
|
||||
# Validate all IDs are integers
|
||||
try:
|
||||
ids = [int(id) for id in ids]
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="All room IDs must be integers")
|
||||
|
||||
# Check if all rooms exist
|
||||
rooms = db.query(Room).filter(Room.id.in_(ids)).all()
|
||||
found_ids = [room.id for room in rooms]
|
||||
not_found_ids = [id for id in ids if id not in found_ids]
|
||||
|
||||
if not_found_ids:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Rooms with IDs {not_found_ids} not found"
|
||||
)
|
||||
|
||||
# Delete all rooms
|
||||
deleted_count = db.query(Room).filter(Room.id.in_(ids)).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Successfully deleted {deleted_count} room(s)",
|
||||
"data": {
|
||||
"deleted_count": deleted_count,
|
||||
"deleted_ids": ids
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
|
||||
async def upload_room_images(
|
||||
id: int,
|
||||
@@ -435,7 +667,7 @@ async def upload_room_images(
|
||||
@router.delete("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
|
||||
async def delete_room_images(
|
||||
id: int,
|
||||
image_url: str,
|
||||
image_url: str = Query(..., description="Image URL or path to delete"),
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -445,12 +677,39 @@ async def delete_room_images(
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
# Update room images (images are stored on Room, not RoomType)
|
||||
# Normalize the incoming image_url to extract the path
|
||||
# Handle both full URLs and relative paths
|
||||
normalized_url = image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
# Extract path from URL
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(image_url)
|
||||
normalized_url = parsed.path
|
||||
|
||||
# Normalize paths for comparison (ensure leading slash)
|
||||
if not normalized_url.startswith('/'):
|
||||
normalized_url = f"/{normalized_url}"
|
||||
|
||||
# Get filename from normalized path
|
||||
filename = Path(normalized_url).name
|
||||
|
||||
# Update room images - compare by filename or full path
|
||||
existing_images = room.images or []
|
||||
updated_images = [img for img in existing_images if img != image_url]
|
||||
updated_images = []
|
||||
|
||||
for img in existing_images:
|
||||
# Normalize stored image path
|
||||
stored_path = img if img.startswith('/') else f"/{img}"
|
||||
stored_filename = Path(stored_path).name
|
||||
|
||||
# Compare by filename or full path
|
||||
# Keep images that don't match
|
||||
if (img != normalized_url and
|
||||
stored_path != normalized_url and
|
||||
stored_filename != filename):
|
||||
updated_images.append(img)
|
||||
|
||||
# Delete file from disk
|
||||
filename = Path(image_url).name
|
||||
file_path = Path(__file__).parent.parent.parent / "uploads" / "rooms" / filename
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
302
Backend/src/routes/system_settings_routes.py
Normal file
302
Backend/src/routes/system_settings_routes.py
Normal file
@@ -0,0 +1,302 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.system_settings import SystemSettings
|
||||
|
||||
router = APIRouter(prefix="/admin/system-settings", tags=["admin-system-settings"])
|
||||
|
||||
|
||||
@router.get("/currency")
|
||||
async def get_platform_currency(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get platform currency setting (public endpoint for frontend)"""
|
||||
try:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "platform_currency"
|
||||
).first()
|
||||
|
||||
if not setting:
|
||||
# Default to VND if not set
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"currency": "VND",
|
||||
"updated_at": None,
|
||||
"updated_by": None
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"currency": setting.value,
|
||||
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
|
||||
"updated_by": setting.updated_by.full_name if setting.updated_by else None
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/currency")
|
||||
async def update_platform_currency(
|
||||
currency_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update platform currency (Admin only)"""
|
||||
try:
|
||||
currency = currency_data.get("currency", "").upper()
|
||||
|
||||
# Validate currency code
|
||||
if not currency or len(currency) != 3 or not currency.isalpha():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid currency code. Must be a 3-letter ISO 4217 code (e.g., USD, EUR, VND)"
|
||||
)
|
||||
|
||||
# Get or create setting
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "platform_currency"
|
||||
).first()
|
||||
|
||||
if setting:
|
||||
setting.value = currency
|
||||
setting.updated_by_id = current_user.id
|
||||
else:
|
||||
setting = SystemSettings(
|
||||
key="platform_currency",
|
||||
value=currency,
|
||||
description="Platform-wide currency setting for displaying prices across the application",
|
||||
updated_by_id=current_user.id
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Platform currency updated successfully",
|
||||
"data": {
|
||||
"currency": setting.value,
|
||||
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
|
||||
"updated_by": setting.updated_by.full_name if setting.updated_by else None
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all system settings (Admin only)"""
|
||||
try:
|
||||
settings = db.query(SystemSettings).all()
|
||||
|
||||
result = []
|
||||
for setting in settings:
|
||||
result.append({
|
||||
"key": setting.key,
|
||||
"value": setting.value,
|
||||
"description": setting.description,
|
||||
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
|
||||
"updated_by": setting.updated_by.full_name if setting.updated_by else None
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"settings": result
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/stripe")
|
||||
async def get_stripe_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get Stripe payment settings (Admin only)"""
|
||||
try:
|
||||
secret_key_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_secret_key"
|
||||
).first()
|
||||
|
||||
publishable_key_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_publishable_key"
|
||||
).first()
|
||||
|
||||
webhook_secret_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_webhook_secret"
|
||||
).first()
|
||||
|
||||
# Mask secret keys 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 = {
|
||||
"stripe_secret_key": "",
|
||||
"stripe_publishable_key": "",
|
||||
"stripe_webhook_secret": "",
|
||||
"stripe_secret_key_masked": "",
|
||||
"stripe_webhook_secret_masked": "",
|
||||
"has_secret_key": False,
|
||||
"has_publishable_key": False,
|
||||
"has_webhook_secret": False,
|
||||
}
|
||||
|
||||
if secret_key_setting:
|
||||
result["stripe_secret_key"] = secret_key_setting.value
|
||||
result["stripe_secret_key_masked"] = mask_key(secret_key_setting.value)
|
||||
result["has_secret_key"] = bool(secret_key_setting.value)
|
||||
result["updated_at"] = secret_key_setting.updated_at.isoformat() if secret_key_setting.updated_at else None
|
||||
result["updated_by"] = secret_key_setting.updated_by.full_name if secret_key_setting.updated_by else None
|
||||
|
||||
if publishable_key_setting:
|
||||
result["stripe_publishable_key"] = publishable_key_setting.value
|
||||
result["has_publishable_key"] = bool(publishable_key_setting.value)
|
||||
|
||||
if webhook_secret_setting:
|
||||
result["stripe_webhook_secret"] = webhook_secret_setting.value
|
||||
result["stripe_webhook_secret_masked"] = mask_key(webhook_secret_setting.value)
|
||||
result["has_webhook_secret"] = bool(webhook_secret_setting.value)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/stripe")
|
||||
async def update_stripe_settings(
|
||||
stripe_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update Stripe payment settings (Admin only)"""
|
||||
try:
|
||||
secret_key = stripe_data.get("stripe_secret_key", "").strip()
|
||||
publishable_key = stripe_data.get("stripe_publishable_key", "").strip()
|
||||
webhook_secret = stripe_data.get("stripe_webhook_secret", "").strip()
|
||||
|
||||
# Validate secret key format (should start with sk_)
|
||||
if secret_key and not secret_key.startswith("sk_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe secret key format. Must start with 'sk_'"
|
||||
)
|
||||
|
||||
# Validate publishable key format (should start with pk_)
|
||||
if publishable_key and not publishable_key.startswith("pk_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe publishable key format. Must start with 'pk_'"
|
||||
)
|
||||
|
||||
# Validate webhook secret format (should start with whsec_)
|
||||
if webhook_secret and not webhook_secret.startswith("whsec_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe webhook secret format. Must start with 'whsec_'"
|
||||
)
|
||||
|
||||
# Update or create secret key setting
|
||||
if secret_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_secret_key"
|
||||
).first()
|
||||
|
||||
if setting:
|
||||
setting.value = secret_key
|
||||
setting.updated_by_id = current_user.id
|
||||
else:
|
||||
setting = SystemSettings(
|
||||
key="stripe_secret_key",
|
||||
value=secret_key,
|
||||
description="Stripe secret key for processing payments",
|
||||
updated_by_id=current_user.id
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create publishable key setting
|
||||
if publishable_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_publishable_key"
|
||||
).first()
|
||||
|
||||
if setting:
|
||||
setting.value = publishable_key
|
||||
setting.updated_by_id = current_user.id
|
||||
else:
|
||||
setting = SystemSettings(
|
||||
key="stripe_publishable_key",
|
||||
value=publishable_key,
|
||||
description="Stripe publishable key for frontend payment forms",
|
||||
updated_by_id=current_user.id
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create webhook secret setting
|
||||
if webhook_secret:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_webhook_secret"
|
||||
).first()
|
||||
|
||||
if setting:
|
||||
setting.value = webhook_secret
|
||||
setting.updated_by_id = current_user.id
|
||||
else:
|
||||
setting = SystemSettings(
|
||||
key="stripe_webhook_secret",
|
||||
value=webhook_secret,
|
||||
description="Stripe webhook secret for verifying webhook events",
|
||||
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": "Stripe settings updated successfully",
|
||||
"data": {
|
||||
"stripe_secret_key": secret_key if secret_key else "",
|
||||
"stripe_publishable_key": publishable_key,
|
||||
"stripe_webhook_secret": webhook_secret if webhook_secret else "",
|
||||
"stripe_secret_key_masked": mask_key(secret_key) if secret_key else "",
|
||||
"stripe_webhook_secret_masked": mask_key(webhook_secret) if webhook_secret else "",
|
||||
"has_secret_key": bool(secret_key),
|
||||
"has_publishable_key": bool(publishable_key),
|
||||
"has_webhook_secret": bool(webhook_secret),
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -66,6 +66,7 @@ async def get_users(
|
||||
"phone_number": user.phone, # For frontend compatibility
|
||||
"address": user.address,
|
||||
"avatar": user.avatar,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"is_active": user.is_active,
|
||||
"status": "active" if user.is_active else "inactive",
|
||||
"role_id": user.role_id,
|
||||
@@ -117,6 +118,7 @@ async def get_user_by_id(
|
||||
"phone_number": user.phone,
|
||||
"address": user.address,
|
||||
"avatar": user.avatar,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"is_active": user.is_active,
|
||||
"status": "active" if user.is_active else "inactive",
|
||||
"role_id": user.role_id,
|
||||
@@ -194,6 +196,7 @@ async def create_user(
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"role_id": user.role_id,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
@@ -248,6 +251,10 @@ async def update_user(
|
||||
user.role_id = role_map.get(user_data["role"], 3)
|
||||
if "status" in user_data and current_user.role_id == 1:
|
||||
user.is_active = user_data["status"] == "active"
|
||||
if "currency" in user_data:
|
||||
currency = user_data["currency"]
|
||||
if len(currency) == 3 and currency.isalpha():
|
||||
user.currency = currency.upper()
|
||||
if "password" in user_data:
|
||||
password_bytes = user_data["password"].encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
@@ -263,6 +270,7 @@ async def update_user(
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"role_id": user.role_id,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user