update
This commit is contained in:
Binary file not shown.
BIN
Backend/src/routes/__pycache__/audit_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/audit_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.
BIN
Backend/src/routes/__pycache__/privacy_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/privacy_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
120
Backend/src/routes/admin_privacy_routes.py
Normal file
120
Backend/src/routes/admin_privacy_routes.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import authorize_roles
|
||||
from ..models.user import User
|
||||
from ..schemas.admin_privacy import (
|
||||
CookieIntegrationSettings,
|
||||
CookieIntegrationSettingsResponse,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
)
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
|
||||
|
||||
router = APIRouter(prefix="/admin/privacy", tags=["admin-privacy"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cookie-policy",
|
||||
response_model=CookiePolicySettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def get_cookie_policy(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(authorize_roles("admin")),
|
||||
) -> CookiePolicySettingsResponse:
|
||||
"""
|
||||
Get global cookie policy configuration (admin only).
|
||||
"""
|
||||
settings = privacy_admin_service.get_policy_settings(db)
|
||||
policy = privacy_admin_service.get_or_create_policy(db)
|
||||
updated_by_name = (
|
||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
||||
)
|
||||
|
||||
return CookiePolicySettingsResponse(
|
||||
data=settings,
|
||||
updated_at=policy.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/cookie-policy",
|
||||
response_model=CookiePolicySettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def update_cookie_policy(
|
||||
payload: CookiePolicySettings,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
) -> CookiePolicySettingsResponse:
|
||||
"""
|
||||
Update global cookie policy configuration (admin only).
|
||||
"""
|
||||
policy = privacy_admin_service.update_policy(db, payload, current_user)
|
||||
settings = privacy_admin_service.get_policy_settings(db)
|
||||
updated_by_name = (
|
||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
||||
)
|
||||
|
||||
return CookiePolicySettingsResponse(
|
||||
data=settings,
|
||||
updated_at=policy.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/integrations",
|
||||
response_model=CookieIntegrationSettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def get_cookie_integrations(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(authorize_roles("admin")),
|
||||
) -> CookieIntegrationSettingsResponse:
|
||||
"""
|
||||
Get IDs for third-party integrations (admin only).
|
||||
"""
|
||||
settings = privacy_admin_service.get_integration_settings(db)
|
||||
cfg = privacy_admin_service.get_or_create_integrations(db)
|
||||
updated_by_name = (
|
||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
||||
)
|
||||
|
||||
return CookieIntegrationSettingsResponse(
|
||||
data=settings,
|
||||
updated_at=cfg.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/integrations",
|
||||
response_model=CookieIntegrationSettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def update_cookie_integrations(
|
||||
payload: CookieIntegrationSettings,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
) -> CookieIntegrationSettingsResponse:
|
||||
"""
|
||||
Update IDs for third-party integrations (admin only).
|
||||
"""
|
||||
cfg = privacy_admin_service.update_integrations(db, payload, current_user)
|
||||
settings = privacy_admin_service.get_integration_settings(db)
|
||||
updated_by_name = (
|
||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
||||
)
|
||||
|
||||
return CookieIntegrationSettingsResponse(
|
||||
data=settings,
|
||||
updated_at=cfg.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
239
Backend/src/routes/audit_routes.py
Normal file
239
Backend/src/routes/audit_routes.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, or_, func
|
||||
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.audit_log import AuditLog
|
||||
|
||||
router = APIRouter(prefix="/audit-logs", tags=["audit-logs"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_audit_logs(
|
||||
action: Optional[str] = Query(None, description="Filter by action"),
|
||||
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
||||
user_id: Optional[int] = Query(None, description="Filter by user ID"),
|
||||
status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
|
||||
search: Optional[str] = Query(None, description="Search in action, resource_type, or details"),
|
||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit logs (Admin only)"""
|
||||
try:
|
||||
query = db.query(AuditLog)
|
||||
|
||||
# Apply filters
|
||||
if action:
|
||||
query = query.filter(AuditLog.action.like(f"%{action}%"))
|
||||
|
||||
if resource_type:
|
||||
query = query.filter(AuditLog.resource_type == resource_type)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(AuditLog.user_id == user_id)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(AuditLog.status == status_filter)
|
||||
|
||||
if search:
|
||||
search_filter = or_(
|
||||
AuditLog.action.like(f"%{search}%"),
|
||||
AuditLog.resource_type.like(f"%{search}%"),
|
||||
AuditLog.ip_address.like(f"%{search}%")
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
# Date range filter
|
||||
if start_date:
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= start)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
# Set to end of day
|
||||
end = end.replace(hour=23, minute=59, second=59)
|
||||
query = query.filter(AuditLog.created_at <= end)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination and ordering
|
||||
offset = (page - 1) * limit
|
||||
logs = query.order_by(desc(AuditLog.created_at)).offset(offset).limit(limit).all()
|
||||
|
||||
# Format response
|
||||
result = []
|
||||
for log in logs:
|
||||
log_dict = {
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"resource_type": log.resource_type,
|
||||
"resource_id": log.resource_id,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"request_id": log.request_id,
|
||||
"details": log.details,
|
||||
"status": log.status,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
|
||||
# Add user info if available
|
||||
if log.user:
|
||||
log_dict["user"] = {
|
||||
"id": log.user.id,
|
||||
"full_name": log.user.full_name,
|
||||
"email": log.user.email,
|
||||
}
|
||||
|
||||
result.append(log_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"logs": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_audit_stats(
|
||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit log statistics (Admin only)"""
|
||||
try:
|
||||
query = db.query(AuditLog)
|
||||
|
||||
# Date range filter
|
||||
if start_date:
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= start)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
end = end.replace(hour=23, minute=59, second=59)
|
||||
query = query.filter(AuditLog.created_at <= end)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get statistics
|
||||
total_logs = query.count()
|
||||
success_count = query.filter(AuditLog.status == "success").count()
|
||||
failed_count = query.filter(AuditLog.status == "failed").count()
|
||||
error_count = query.filter(AuditLog.status == "error").count()
|
||||
|
||||
# Get top actions
|
||||
top_actions = (
|
||||
db.query(
|
||||
AuditLog.action,
|
||||
func.count(AuditLog.id).label("count")
|
||||
)
|
||||
.group_by(AuditLog.action)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get top resource types
|
||||
top_resource_types = (
|
||||
db.query(
|
||||
AuditLog.resource_type,
|
||||
func.count(AuditLog.id).label("count")
|
||||
)
|
||||
.group_by(AuditLog.resource_type)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total": total_logs,
|
||||
"by_status": {
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"error": error_count,
|
||||
},
|
||||
"top_actions": [{"action": action, "count": count} for action, count in top_actions],
|
||||
"top_resource_types": [{"resource_type": rt, "count": count} for rt, count in top_resource_types],
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_audit_log_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit log by ID (Admin only)"""
|
||||
try:
|
||||
log = db.query(AuditLog).filter(AuditLog.id == id).first()
|
||||
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="Audit log not found")
|
||||
|
||||
log_dict = {
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"resource_type": log.resource_type,
|
||||
"resource_id": log.resource_id,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"request_id": log.request_id,
|
||||
"details": log.details,
|
||||
"status": log.status,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
|
||||
if log.user:
|
||||
log_dict["user"] = {
|
||||
"id": log.user.id,
|
||||
"full_name": log.user.full_name,
|
||||
"email": log.user.email,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"log": log_dict}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -163,7 +163,12 @@ async def get_profile(
|
||||
"""Get current user profile"""
|
||||
try:
|
||||
user = await auth_service.get_profile(db, current_user.id)
|
||||
return user
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"user": user
|
||||
}
|
||||
}
|
||||
except ValueError as e:
|
||||
if "User not found" in str(e):
|
||||
raise HTTPException(
|
||||
@@ -176,6 +181,46 @@ async def get_profile(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
async def update_profile(
|
||||
profile_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update current user profile"""
|
||||
try:
|
||||
user = await auth_service.update_profile(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
full_name=profile_data.get("full_name"),
|
||||
email=profile_data.get("email"),
|
||||
phone_number=profile_data.get("phone_number"),
|
||||
password=profile_data.get("password"),
|
||||
current_password=profile_data.get("currentPassword")
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Profile updated successfully",
|
||||
"data": {
|
||||
"user": user
|
||||
}
|
||||
}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if "not found" in error_message.lower():
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
raise HTTPException(
|
||||
status_code=status_code,
|
||||
detail=error_message
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"An error occurred: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/forgot-password", response_model=MessageResponse)
|
||||
async def forgot_password(
|
||||
request: ForgotPasswordRequest,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import os
|
||||
import aiofiles
|
||||
import uuid
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
@@ -215,6 +218,12 @@ async def delete_banner(
|
||||
if not banner:
|
||||
raise HTTPException(status_code=404, detail="Banner not found")
|
||||
|
||||
# Delete image file if it exists and is a local upload
|
||||
if banner.image_url and banner.image_url.startswith('/uploads/banners/'):
|
||||
file_path = Path(__file__).parent.parent.parent / "uploads" / "banners" / Path(banner.image_url).name
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
db.delete(banner)
|
||||
db.commit()
|
||||
|
||||
@@ -227,3 +236,51 @@ async def delete_banner(
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def upload_banner_image(
|
||||
request: Request,
|
||||
image: UploadFile = File(...),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
):
|
||||
"""Upload banner image (Admin only)"""
|
||||
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"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "banners"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename
|
||||
ext = Path(image.filename).suffix
|
||||
filename = f"banner-{uuid.uuid4()}{ext}"
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await image.read()
|
||||
await f.write(content)
|
||||
|
||||
# Return the image URL
|
||||
image_url = f"/uploads/banners/{filename}"
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Image uploaded successfully",
|
||||
"data": {
|
||||
"image_url": image_url,
|
||||
"full_url": full_url
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -4,14 +4,21 @@ from sqlalchemy import and_, or_
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import random
|
||||
import os
|
||||
|
||||
from ..config.database import get_db
|
||||
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_type import RoomType
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import (
|
||||
booking_confirmation_email_template,
|
||||
booking_status_changed_email_template
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/bookings", tags=["bookings"])
|
||||
|
||||
@@ -255,6 +262,33 @@ async def create_booking(
|
||||
# Fetch with relations
|
||||
booking = db.query(Booking).filter(Booking.id == booking.id).first()
|
||||
|
||||
# Send booking confirmation email (non-blocking)
|
||||
try:
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
room = db.query(Room).filter(Room.id == room_id).first()
|
||||
room_type_name = room.room_type.name if room and room.room_type else "Room"
|
||||
|
||||
email_html = booking_confirmation_email_template(
|
||||
booking_number=booking.booking_number,
|
||||
guest_name=current_user.full_name,
|
||||
room_number=room.room_number if room else "N/A",
|
||||
room_type=room_type_name,
|
||||
check_in=check_in.strftime("%B %d, %Y"),
|
||||
check_out=check_out.strftime("%B %d, %Y"),
|
||||
num_guests=guest_count,
|
||||
total_price=float(total_price),
|
||||
requires_deposit=requires_deposit,
|
||||
deposit_amount=deposit_amount if requires_deposit else None,
|
||||
client_url=client_url
|
||||
)
|
||||
await send_email(
|
||||
to=current_user.email,
|
||||
subject=f"Booking Confirmation - {booking.booking_number}",
|
||||
html=email_html
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send booking confirmation email: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"booking": booking},
|
||||
@@ -354,6 +388,23 @@ async def cancel_booking(
|
||||
booking.status = BookingStatus.cancelled
|
||||
db.commit()
|
||||
|
||||
# Send cancellation email (non-blocking)
|
||||
try:
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
email_html = booking_status_changed_email_template(
|
||||
booking_number=booking.booking_number,
|
||||
guest_name=booking.user.full_name if booking.user else "Guest",
|
||||
status="cancelled",
|
||||
client_url=client_url
|
||||
)
|
||||
await send_email(
|
||||
to=booking.user.email if booking.user else None,
|
||||
subject=f"Booking Cancelled - {booking.booking_number}",
|
||||
html=email_html
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send cancellation email: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"booking": booking}
|
||||
@@ -378,6 +429,7 @@ async def update_booking(
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
|
||||
old_status = booking.status
|
||||
status_value = booking_data.get("status")
|
||||
if status_value:
|
||||
try:
|
||||
@@ -388,6 +440,24 @@ async def update_booking(
|
||||
db.commit()
|
||||
db.refresh(booking)
|
||||
|
||||
# Send status change email if status changed (non-blocking)
|
||||
if status_value and old_status != booking.status:
|
||||
try:
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
email_html = booking_status_changed_email_template(
|
||||
booking_number=booking.booking_number,
|
||||
guest_name=booking.user.full_name if booking.user else "Guest",
|
||||
status=booking.status.value,
|
||||
client_url=client_url
|
||||
)
|
||||
await send_email(
|
||||
to=booking.user.email if booking.user else None,
|
||||
subject=f"Booking Status Updated - {booking.booking_number}",
|
||||
html=email_html
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send status change email: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Booking updated successfully",
|
||||
|
||||
@@ -2,12 +2,16 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.settings import settings
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ..models.booking import Booking
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import payment_confirmation_email_template
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["payments"])
|
||||
|
||||
@@ -85,6 +89,63 @@ async def get_payments(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/booking/{booking_id}")
|
||||
async def get_payments_by_booking_id(
|
||||
booking_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all payments 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 - users can only see their own bookings unless admin
|
||||
if current_user.role_id != 1 and booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# Get all payments for this booking
|
||||
payments = db.query(Payment).filter(Payment.booking_id == booking_id).order_by(Payment.created_at.desc()).all()
|
||||
|
||||
result = []
|
||||
for payment in payments:
|
||||
payment_dict = {
|
||||
"id": payment.id,
|
||||
"booking_id": payment.booking_id,
|
||||
"amount": float(payment.amount) if payment.amount else 0.0,
|
||||
"payment_method": payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method,
|
||||
"payment_type": payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type,
|
||||
"deposit_percentage": payment.deposit_percentage,
|
||||
"related_payment_id": payment.related_payment_id,
|
||||
"payment_status": payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status,
|
||||
"transaction_id": payment.transaction_id,
|
||||
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
|
||||
"notes": payment.notes,
|
||||
"created_at": payment.created_at.isoformat() if payment.created_at else None,
|
||||
}
|
||||
|
||||
if payment.booking:
|
||||
payment_dict["booking"] = {
|
||||
"id": payment.booking.id,
|
||||
"booking_number": payment.booking.booking_number,
|
||||
}
|
||||
|
||||
result.append(payment_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"payments": result
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_payment_by_id(
|
||||
id: int,
|
||||
@@ -169,11 +230,32 @@ async def create_payment(
|
||||
# If marked as paid, update status
|
||||
if payment_data.get("mark_as_paid"):
|
||||
payment.payment_status = PaymentStatus.completed
|
||||
payment.payment_date = datetime.utcnow()
|
||||
|
||||
db.add(payment)
|
||||
db.commit()
|
||||
db.refresh(payment)
|
||||
|
||||
# Send payment confirmation email if payment was marked as paid (non-blocking)
|
||||
if payment.payment_status == PaymentStatus.completed 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=float(payment.amount),
|
||||
payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method),
|
||||
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:
|
||||
print(f"Failed to send payment confirmation email: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Payment created successfully",
|
||||
@@ -209,6 +291,7 @@ async def update_payment_status(
|
||||
if status_data.get("transaction_id"):
|
||||
payment.transaction_id = status_data["transaction_id"]
|
||||
|
||||
old_status = payment.payment_status
|
||||
if status_data.get("mark_as_paid"):
|
||||
payment.payment_status = PaymentStatus.completed
|
||||
payment.payment_date = datetime.utcnow()
|
||||
@@ -216,6 +299,37 @@ async def update_payment_status(
|
||||
db.commit()
|
||||
db.refresh(payment)
|
||||
|
||||
# Send payment confirmation email if payment was just completed (non-blocking)
|
||||
if payment.payment_status == PaymentStatus.completed and old_status != PaymentStatus.completed:
|
||||
try:
|
||||
# Refresh booking relationship
|
||||
payment = db.query(Payment).filter(Payment.id == id).first()
|
||||
if payment.booking and payment.booking.user:
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
email_html = payment_confirmation_email_template(
|
||||
booking_number=payment.booking.booking_number,
|
||||
guest_name=payment.booking.user.full_name,
|
||||
amount=float(payment.amount),
|
||||
payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method),
|
||||
transaction_id=payment.transaction_id,
|
||||
client_url=client_url
|
||||
)
|
||||
await send_email(
|
||||
to=payment.booking.user.email,
|
||||
subject=f"Payment Confirmed - {payment.booking.booking_number}",
|
||||
html=email_html
|
||||
)
|
||||
|
||||
# If this is a deposit payment, update booking deposit_paid status
|
||||
if payment.payment_type == PaymentType.deposit and payment.booking:
|
||||
payment.booking.deposit_paid = True
|
||||
# Optionally auto-confirm booking if deposit is paid
|
||||
if payment.booking.status == BookingStatus.pending:
|
||||
payment.booking.status = BookingStatus.confirmed
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(f"Failed to send payment confirmation email: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Payment status updated successfully",
|
||||
|
||||
111
Backend/src/routes/privacy_routes.py
Normal file
111
Backend/src/routes/privacy_routes.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..config.settings import settings
|
||||
from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie
|
||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||
from ..schemas.privacy import (
|
||||
CookieCategoryPreferences,
|
||||
CookieConsent,
|
||||
CookieConsentResponse,
|
||||
UpdateCookieConsentRequest,
|
||||
)
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/privacy", tags=["privacy"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cookie-consent",
|
||||
response_model=CookieConsentResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
|
||||
"""
|
||||
Return the current cookie consent preferences.
|
||||
Reads from the cookie (if present) or returns default (necessary only).
|
||||
"""
|
||||
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
||||
consent = _parse_consent_cookie(raw_cookie)
|
||||
|
||||
# Ensure necessary is always true
|
||||
consent.categories.necessary = True
|
||||
|
||||
return CookieConsentResponse(data=consent)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/cookie-consent",
|
||||
response_model=CookieConsentResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def update_cookie_consent(
|
||||
request: UpdateCookieConsentRequest, response: Response
|
||||
) -> CookieConsentResponse:
|
||||
"""
|
||||
Update cookie consent preferences.
|
||||
|
||||
The 'necessary' category is controlled by the server and always true.
|
||||
"""
|
||||
# Build categories from existing cookie (if any) so partial updates work
|
||||
existing_raw = response.headers.get("cookie") # usually empty here
|
||||
# We can't reliably read cookies from the response; rely on defaults.
|
||||
# For the purposes of this API, we always start from defaults and then
|
||||
# override with the request payload.
|
||||
categories = CookieCategoryPreferences()
|
||||
|
||||
if request.analytics is not None:
|
||||
categories.analytics = request.analytics
|
||||
if request.marketing is not None:
|
||||
categories.marketing = request.marketing
|
||||
if request.preferences is not None:
|
||||
categories.preferences = request.preferences
|
||||
|
||||
# 'necessary' enforced server-side
|
||||
categories.necessary = True
|
||||
|
||||
consent = CookieConsent(categories=categories, has_decided=True)
|
||||
|
||||
# Persist consent as a secure, HttpOnly cookie
|
||||
response.set_cookie(
|
||||
key=COOKIE_CONSENT_COOKIE_NAME,
|
||||
value=consent.model_dump_json(),
|
||||
httponly=True,
|
||||
secure=settings.is_production,
|
||||
samesite="lax",
|
||||
max_age=365 * 24 * 60 * 60, # 1 year
|
||||
path="/",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Cookie consent updated: analytics=%s, marketing=%s, preferences=%s",
|
||||
consent.categories.analytics,
|
||||
consent.categories.marketing,
|
||||
consent.categories.preferences,
|
||||
)
|
||||
|
||||
return CookieConsentResponse(data=consent)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/config",
|
||||
response_model=PublicPrivacyConfigResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def get_public_privacy_config(
|
||||
db: Session = Depends(get_db),
|
||||
) -> PublicPrivacyConfigResponse:
|
||||
"""
|
||||
Public privacy configuration for the frontend:
|
||||
- Global policy flags
|
||||
- Public integration IDs (e.g. GA measurement ID)
|
||||
"""
|
||||
config = privacy_admin_service.get_public_privacy_config(db)
|
||||
return PublicPrivacyConfigResponse(data=config)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ from ..models.user import User
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment, PaymentStatus
|
||||
from ..models.room import Room
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["reports"])
|
||||
|
||||
@@ -140,6 +142,33 @@ async def get_reports(
|
||||
for room_id, room_number, bookings, revenue in top_rooms_data
|
||||
]
|
||||
|
||||
# Service usage statistics
|
||||
service_usage_query = db.query(
|
||||
Service.id,
|
||||
Service.name,
|
||||
func.count(ServiceUsage.id).label('usage_count'),
|
||||
func.sum(ServiceUsage.total_price).label('total_revenue')
|
||||
).join(ServiceUsage, Service.id == ServiceUsage.service_id)
|
||||
|
||||
if start_date:
|
||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date >= start_date)
|
||||
if end_date:
|
||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date)
|
||||
|
||||
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(
|
||||
func.sum(ServiceUsage.total_price).desc()
|
||||
).limit(10).all()
|
||||
|
||||
service_usage = [
|
||||
{
|
||||
"service_id": service_id,
|
||||
"service_name": service_name,
|
||||
"usage_count": int(usage_count or 0),
|
||||
"total_revenue": float(total_revenue or 0)
|
||||
}
|
||||
for service_id, service_name, usage_count, total_revenue in service_usage_data
|
||||
]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"success": True,
|
||||
@@ -152,6 +181,7 @@ async def get_reports(
|
||||
"revenue_by_date": revenue_by_date if revenue_by_date else None,
|
||||
"bookings_by_status": bookings_by_status,
|
||||
"top_rooms": top_rooms if top_rooms else None,
|
||||
"service_usage": service_usage if service_usage else None,
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -221,6 +251,171 @@ async def get_dashboard_stats(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/customer/dashboard")
|
||||
async def get_customer_dashboard_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get customer dashboard statistics"""
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Total bookings count for user
|
||||
total_bookings = db.query(Booking).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).count()
|
||||
|
||||
# Total spending (sum of completed payments from user's bookings)
|
||||
user_bookings = db.query(Booking.id).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).subquery()
|
||||
|
||||
total_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
# Currently staying (checked_in bookings)
|
||||
now = datetime.utcnow()
|
||||
currently_staying = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.status == BookingStatus.checked_in,
|
||||
Booking.check_in_date <= now,
|
||||
Booking.check_out_date >= now
|
||||
)
|
||||
).count()
|
||||
|
||||
# Upcoming bookings (confirmed/pending with check_in_date in future)
|
||||
upcoming_bookings_query = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]),
|
||||
Booking.check_in_date > now
|
||||
)
|
||||
).order_by(Booking.check_in_date.asc()).limit(5).all()
|
||||
|
||||
upcoming_bookings = []
|
||||
for booking in upcoming_bookings_query:
|
||||
booking_dict = {
|
||||
"id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
"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,
|
||||
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0.0,
|
||||
}
|
||||
|
||||
if booking.room:
|
||||
booking_dict["room"] = {
|
||||
"id": booking.room.id,
|
||||
"room_number": booking.room.room_number,
|
||||
"room_type": {
|
||||
"name": booking.room.room_type.name if booking.room.room_type else None
|
||||
}
|
||||
}
|
||||
|
||||
upcoming_bookings.append(booking_dict)
|
||||
|
||||
# Recent activity (last 5 bookings ordered by created_at)
|
||||
recent_bookings_query = db.query(Booking).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).order_by(Booking.created_at.desc()).limit(5).all()
|
||||
|
||||
recent_activity = []
|
||||
for booking in recent_bookings_query:
|
||||
activity_type = None
|
||||
if booking.status == BookingStatus.checked_out:
|
||||
activity_type = "Check-out"
|
||||
elif booking.status == BookingStatus.checked_in:
|
||||
activity_type = "Check-in"
|
||||
elif booking.status == BookingStatus.confirmed:
|
||||
activity_type = "Booking Confirmed"
|
||||
elif booking.status == BookingStatus.pending:
|
||||
activity_type = "Booking"
|
||||
else:
|
||||
activity_type = "Booking"
|
||||
|
||||
activity_dict = {
|
||||
"action": activity_type,
|
||||
"booking_id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
||||
}
|
||||
|
||||
if booking.room:
|
||||
activity_dict["room"] = {
|
||||
"room_number": booking.room.room_number,
|
||||
}
|
||||
|
||||
recent_activity.append(activity_dict)
|
||||
|
||||
# Calculate percentage change (placeholder - can be enhanced)
|
||||
# For now, compare last month vs this month
|
||||
last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0)
|
||||
last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
|
||||
|
||||
last_month_bookings = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.created_at >= last_month_start,
|
||||
Booking.created_at <= last_month_end
|
||||
)
|
||||
).count()
|
||||
|
||||
this_month_bookings = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0),
|
||||
Booking.created_at <= now
|
||||
)
|
||||
).count()
|
||||
|
||||
booking_change_percentage = 0
|
||||
if last_month_bookings > 0:
|
||||
booking_change_percentage = ((this_month_bookings - last_month_bookings) / last_month_bookings) * 100
|
||||
|
||||
last_month_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
Payment.payment_date >= last_month_start,
|
||||
Payment.payment_date <= last_month_end
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
this_month_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0),
|
||||
Payment.payment_date <= now
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
spending_change_percentage = 0
|
||||
if last_month_spending > 0:
|
||||
spending_change_percentage = ((this_month_spending - last_month_spending) / last_month_spending) * 100
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_bookings": total_bookings,
|
||||
"total_spending": float(total_spending),
|
||||
"currently_staying": currently_staying,
|
||||
"upcoming_bookings": upcoming_bookings,
|
||||
"recent_activity": recent_activity,
|
||||
"booking_change_percentage": round(booking_change_percentage, 1),
|
||||
"spending_change_percentage": round(spending_change_percentage, 1),
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/revenue")
|
||||
async def get_revenue_report(
|
||||
start_date: Optional[str] = Query(None),
|
||||
|
||||
Reference in New Issue
Block a user