updates
This commit is contained in:
Binary file not shown.
@@ -200,7 +200,8 @@ from .routes import (
|
||||
room_routes, booking_routes, payment_routes, invoice_routes, banner_routes,
|
||||
favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes,
|
||||
review_routes, user_routes, audit_routes, admin_privacy_routes,
|
||||
system_settings_routes, contact_routes, page_content_routes
|
||||
system_settings_routes, contact_routes, page_content_routes,
|
||||
home_routes, about_routes, contact_content_routes, footer_routes
|
||||
)
|
||||
|
||||
# Legacy routes (maintain backward compatibility)
|
||||
@@ -220,6 +221,10 @@ app.include_router(audit_routes.router, prefix="/api")
|
||||
app.include_router(admin_privacy_routes.router, prefix="/api")
|
||||
app.include_router(system_settings_routes.router, prefix="/api")
|
||||
app.include_router(contact_routes.router, prefix="/api")
|
||||
app.include_router(home_routes.router, prefix="/api")
|
||||
app.include_router(about_routes.router, prefix="/api")
|
||||
app.include_router(contact_content_routes.router, prefix="/api")
|
||||
app.include_router(footer_routes.router, prefix="/api")
|
||||
|
||||
# Versioned routes (v1)
|
||||
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
@@ -238,6 +243,10 @@ app.include_router(audit_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(admin_privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(system_settings_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(contact_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(page_content_routes.router, prefix="/api")
|
||||
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ import os
|
||||
from ..config.database import get_db
|
||||
from ..config.settings import settings
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
@@ -46,10 +47,20 @@ def authorize_roles(*allowed_roles: str):
|
||||
"""
|
||||
Check if user has required role
|
||||
"""
|
||||
def role_checker(current_user: User = Depends(get_current_user)) -> User:
|
||||
# Map role IDs to role names
|
||||
role_map = {1: "admin", 2: "staff", 3: "customer"}
|
||||
user_role_name = role_map.get(current_user.role_id)
|
||||
def role_checker(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
# Query the role from database instead of using hardcoded IDs
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
|
||||
if not role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User role not found"
|
||||
)
|
||||
|
||||
user_role_name = role.name
|
||||
|
||||
if user_role_name not in allowed_roles:
|
||||
raise HTTPException(
|
||||
|
||||
Binary file not shown.
@@ -41,6 +41,7 @@ class PageContent(Base):
|
||||
social_links = Column(Text, nullable=True) # JSON: facebook, twitter, instagram, etc.
|
||||
footer_links = Column(Text, nullable=True) # JSON: quick links, support links
|
||||
badges = Column(Text, nullable=True) # JSON: array of badges with text and icon
|
||||
copyright_text = Column(Text, nullable=True) # Copyright text with {YEAR} placeholder for automatic year
|
||||
|
||||
# Home page specific
|
||||
hero_title = Column(String(500), nullable=True)
|
||||
@@ -51,6 +52,57 @@ class PageContent(Base):
|
||||
story_content = Column(Text, nullable=True)
|
||||
values = Column(Text, nullable=True) # JSON array of values
|
||||
features = Column(Text, nullable=True) # JSON array of features
|
||||
about_hero_image = Column(Text, nullable=True) # Hero image for about page
|
||||
mission = Column(Text, nullable=True) # Mission statement
|
||||
vision = Column(Text, nullable=True) # Vision statement
|
||||
team = Column(Text, nullable=True) # JSON array of team members with name, role, image, bio, social_links
|
||||
timeline = Column(Text, nullable=True) # JSON array of timeline events with year, title, description, image
|
||||
achievements = Column(Text, nullable=True) # JSON array of achievements with icon, title, description, year, image
|
||||
|
||||
# Home page luxury sections
|
||||
luxury_section_title = Column(Text, nullable=True)
|
||||
luxury_section_subtitle = Column(Text, nullable=True)
|
||||
luxury_section_image = Column(Text, nullable=True)
|
||||
luxury_features = Column(Text, nullable=True) # JSON array of features with icon, title, description
|
||||
luxury_gallery_section_title = Column(Text, nullable=True)
|
||||
luxury_gallery_section_subtitle = Column(Text, nullable=True)
|
||||
luxury_gallery = Column(Text, nullable=True) # JSON array of image URLs
|
||||
luxury_testimonials_section_title = Column(Text, nullable=True)
|
||||
luxury_testimonials_section_subtitle = Column(Text, nullable=True)
|
||||
luxury_testimonials = Column(Text, nullable=True) # JSON array of testimonials
|
||||
amenities_section_title = Column(String(500), nullable=True)
|
||||
amenities_section_subtitle = Column(String(1000), nullable=True)
|
||||
amenities = Column(Text, nullable=True) # JSON array of amenities with icon, title, description, image
|
||||
testimonials_section_title = Column(String(500), nullable=True)
|
||||
testimonials_section_subtitle = Column(String(1000), nullable=True)
|
||||
testimonials = Column(Text, nullable=True) # JSON array of testimonials with name, role, image, rating, comment
|
||||
gallery_section_title = Column(String(500), nullable=True)
|
||||
gallery_section_subtitle = Column(String(1000), nullable=True)
|
||||
gallery_images = Column(Text, nullable=True) # JSON array of image URLs
|
||||
about_preview_title = Column(String(500), nullable=True)
|
||||
about_preview_subtitle = Column(String(1000), nullable=True)
|
||||
about_preview_content = Column(Text, nullable=True)
|
||||
about_preview_image = Column(String(1000), nullable=True)
|
||||
stats = Column(Text, nullable=True) # JSON array of stats with number, label, icon
|
||||
|
||||
# Additional luxury sections
|
||||
luxury_services_section_title = Column(Text, nullable=True)
|
||||
luxury_services_section_subtitle = Column(Text, nullable=True)
|
||||
luxury_services = Column(Text, nullable=True) # JSON array of services with icon, title, description, image
|
||||
luxury_experiences_section_title = Column(Text, nullable=True)
|
||||
luxury_experiences_section_subtitle = Column(Text, nullable=True)
|
||||
luxury_experiences = Column(Text, nullable=True) # JSON array of experiences with icon, title, description, image
|
||||
awards_section_title = Column(Text, nullable=True)
|
||||
awards_section_subtitle = Column(Text, nullable=True)
|
||||
awards = Column(Text, nullable=True) # JSON array of awards with icon, title, description, image, year
|
||||
cta_title = Column(Text, nullable=True)
|
||||
cta_subtitle = Column(Text, nullable=True)
|
||||
cta_button_text = Column(Text, nullable=True)
|
||||
cta_button_link = Column(Text, nullable=True)
|
||||
cta_image = Column(Text, nullable=True)
|
||||
partners_section_title = Column(Text, nullable=True)
|
||||
partners_section_subtitle = Column(Text, nullable=True)
|
||||
partners = Column(Text, nullable=True) # JSON array of partners with name, logo, link
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
BIN
Backend/src/routes/__pycache__/about_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/about_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__/footer_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Backend/src/routes/__pycache__/home_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/home_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.
Binary file not shown.
75
Backend/src/routes/about_routes.py
Normal file
75
Backend/src/routes/about_routes.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/about", tags=["about"])
|
||||
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"story_content": content.story_content,
|
||||
"values": json.loads(content.values) if content.values else None,
|
||||
"features": json.loads(content.features) if content.features else None,
|
||||
"about_hero_image": content.about_hero_image,
|
||||
"mission": content.mission,
|
||||
"vision": content.vision,
|
||||
"team": json.loads(content.team) if content.team else None,
|
||||
"timeline": json.loads(content.timeline) if content.timeline else None,
|
||||
"achievements": json.loads(content.achievements) if content.achievements else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_about_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get about page content"""
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching about content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching about content: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -495,6 +495,7 @@ async def upload_avatar(
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Avatar uploaded successfully",
|
||||
"data": {
|
||||
|
||||
@@ -246,11 +246,25 @@ async def upload_banner_image(
|
||||
):
|
||||
"""Upload banner image (Admin only)"""
|
||||
try:
|
||||
# Validate file exists
|
||||
if not image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No file provided"
|
||||
)
|
||||
|
||||
# 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"
|
||||
detail=f"File must be an image. Received: {image.content_type}"
|
||||
)
|
||||
|
||||
# Validate filename
|
||||
if not image.filename:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
@@ -258,21 +272,27 @@ async def upload_banner_image(
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename
|
||||
ext = Path(image.filename).suffix
|
||||
ext = Path(image.filename).suffix or '.jpg'
|
||||
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()
|
||||
if not content:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File is empty"
|
||||
)
|
||||
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 {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Image uploaded successfully",
|
||||
"data": {
|
||||
|
||||
@@ -61,6 +61,11 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}}
|
||||
.company-logo {{
|
||||
max-width: 150px;
|
||||
max-height: 80px;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #ffffff;
|
||||
padding: 30px;
|
||||
@@ -109,6 +114,8 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
{f'<img src="{invoice.get("company_logo_url")}" alt="Company Logo" class="company-logo" />' if invoice.get('company_logo_url') else ''}
|
||||
{f'<h2 style="margin: 10px 0; color: #0f0f0f;">{invoice.get("company_name", "")}</h2>' if invoice.get('company_name') else ''}
|
||||
<h1>{invoice_type}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -139,6 +146,7 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st
|
||||
<div class="total">
|
||||
<p>Subtotal: {invoice.get('subtotal', 0):.2f}</p>
|
||||
{f'<p style="color: #059669;">Discount: -{invoice.get("discount_amount", 0):.2f}</p>' if invoice.get('discount_amount', 0) > 0 else ''}
|
||||
{f'<p style="color: #059669; font-size: 14px;">Promotion Code: {invoice.get("promotion_code", "")}</p>' if invoice.get('promotion_code') else ''}
|
||||
<p>Tax: {invoice.get('tax_amount', 0):.2f}</p>
|
||||
<p><strong>Total Amount: {invoice.get('total_amount', 0):.2f}</strong></p>
|
||||
<p>Amount Paid: {invoice.get('amount_paid', 0):.2f}</p>
|
||||
@@ -729,19 +737,31 @@ async def create_booking(
|
||||
# Get discount from booking
|
||||
booking_discount = float(booking.discount_amount) if booking.discount_amount else 0.0
|
||||
|
||||
# Add promotion code to invoice notes if present
|
||||
invoice_notes = invoice_kwargs.get("notes", "")
|
||||
if booking.promotion_code:
|
||||
promotion_note = f"Promotion Code: {booking.promotion_code}"
|
||||
invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note
|
||||
invoice_kwargs["notes"] = invoice_notes
|
||||
|
||||
# Create invoices based on payment method
|
||||
if payment_method == "cash":
|
||||
# For cash bookings: create invoice for 20% deposit + proforma for 80% remaining
|
||||
deposit_amount = float(total_price) * 0.2
|
||||
remaining_amount = float(total_price) * 0.8
|
||||
|
||||
# Calculate proportional discount for partial invoices
|
||||
# Deposit invoice gets 20% of the discount, proforma gets 80%
|
||||
deposit_discount = booking_discount * 0.2 if booking_discount > 0 else 0.0
|
||||
proforma_discount = booking_discount * 0.8 if booking_discount > 0 else 0.0
|
||||
|
||||
# Create invoice for deposit (20%)
|
||||
deposit_invoice = InvoiceService.create_invoice_from_booking(
|
||||
booking_id=booking.id,
|
||||
db=db,
|
||||
created_by_id=current_user.id,
|
||||
tax_rate=tax_rate,
|
||||
discount_amount=booking_discount,
|
||||
discount_amount=deposit_discount,
|
||||
due_days=30,
|
||||
is_proforma=False,
|
||||
invoice_amount=deposit_amount,
|
||||
@@ -754,7 +774,7 @@ async def create_booking(
|
||||
db=db,
|
||||
created_by_id=current_user.id,
|
||||
tax_rate=tax_rate,
|
||||
discount_amount=booking_discount,
|
||||
discount_amount=proforma_discount,
|
||||
due_days=30,
|
||||
is_proforma=True,
|
||||
invoice_amount=remaining_amount,
|
||||
|
||||
68
Backend/src/routes/contact_content_routes.py
Normal file
68
Backend/src/routes/contact_content_routes.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/contact-content", tags=["contact-content"])
|
||||
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"contact_info": json.loads(content.contact_info) if content.contact_info else None,
|
||||
"map_url": content.map_url,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_contact_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get contact page content"""
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching contact content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching contact content: {str(e)}"
|
||||
)
|
||||
|
||||
63
Backend/src/routes/footer_routes.py
Normal file
63
Backend/src/routes/footer_routes.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/footer", tags=["footer"])
|
||||
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"social_links": json.loads(content.social_links) if content.social_links else None,
|
||||
"footer_links": json.loads(content.footer_links) if content.footer_links else None,
|
||||
"badges": json.loads(content.badges) if content.badges else None,
|
||||
"copyright_text": content.copyright_text,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_footer_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get footer content"""
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching footer content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching footer content: {str(e)}"
|
||||
)
|
||||
|
||||
110
Backend/src/routes/home_routes.py
Normal file
110
Backend/src/routes/home_routes.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/home", tags=["home"])
|
||||
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"hero_title": content.hero_title,
|
||||
"hero_subtitle": content.hero_subtitle,
|
||||
"hero_image": content.hero_image,
|
||||
"amenities_section_title": content.amenities_section_title,
|
||||
"amenities_section_subtitle": content.amenities_section_subtitle,
|
||||
"amenities": json.loads(content.amenities) if content.amenities else None,
|
||||
"testimonials_section_title": content.testimonials_section_title,
|
||||
"testimonials_section_subtitle": content.testimonials_section_subtitle,
|
||||
"testimonials": json.loads(content.testimonials) if content.testimonials else None,
|
||||
"gallery_section_title": content.gallery_section_title,
|
||||
"gallery_section_subtitle": content.gallery_section_subtitle,
|
||||
"gallery_images": json.loads(content.gallery_images) if content.gallery_images else None,
|
||||
"luxury_section_title": content.luxury_section_title,
|
||||
"luxury_section_subtitle": content.luxury_section_subtitle,
|
||||
"luxury_section_image": content.luxury_section_image,
|
||||
"luxury_features": json.loads(content.luxury_features) if content.luxury_features else None,
|
||||
"luxury_gallery_section_title": content.luxury_gallery_section_title,
|
||||
"luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle,
|
||||
"luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None,
|
||||
"luxury_testimonials_section_title": content.luxury_testimonials_section_title,
|
||||
"luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle,
|
||||
"luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None,
|
||||
"about_preview_title": content.about_preview_title,
|
||||
"about_preview_subtitle": content.about_preview_subtitle,
|
||||
"about_preview_content": content.about_preview_content,
|
||||
"about_preview_image": content.about_preview_image,
|
||||
"stats": json.loads(content.stats) if content.stats else None,
|
||||
"luxury_services_section_title": content.luxury_services_section_title,
|
||||
"luxury_services_section_subtitle": content.luxury_services_section_subtitle,
|
||||
"luxury_services": json.loads(content.luxury_services) if content.luxury_services else None,
|
||||
"luxury_experiences_section_title": content.luxury_experiences_section_title,
|
||||
"luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle,
|
||||
"luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None,
|
||||
"awards_section_title": content.awards_section_title,
|
||||
"awards_section_subtitle": content.awards_section_subtitle,
|
||||
"awards": json.loads(content.awards) if content.awards else None,
|
||||
"cta_title": content.cta_title,
|
||||
"cta_subtitle": content.cta_subtitle,
|
||||
"cta_button_text": content.cta_button_text,
|
||||
"cta_button_link": content.cta_button_link,
|
||||
"cta_image": content.cta_image,
|
||||
"partners_section_title": content.partners_section_title,
|
||||
"partners_section_subtitle": content.partners_section_subtitle,
|
||||
"partners": json.loads(content.partners) if content.partners else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_home_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get homepage content"""
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching home content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching home content: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -87,11 +87,38 @@ async def create_invoice(
|
||||
if not booking_id:
|
||||
raise HTTPException(status_code=400, detail="booking_id is required")
|
||||
|
||||
# Ensure booking_id is an integer
|
||||
try:
|
||||
booking_id = int(booking_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="booking_id must be a valid integer")
|
||||
|
||||
# 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")
|
||||
|
||||
# Prepare invoice kwargs
|
||||
invoice_kwargs = {
|
||||
"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"),
|
||||
}
|
||||
|
||||
# Add promotion code to invoice notes if present in booking
|
||||
invoice_notes = invoice_kwargs.get("notes", "")
|
||||
if booking.promotion_code:
|
||||
promotion_note = f"Promotion Code: {booking.promotion_code}"
|
||||
invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note
|
||||
invoice_kwargs["notes"] = invoice_notes
|
||||
|
||||
# Create invoice
|
||||
invoice = InvoiceService.create_invoice_from_booking(
|
||||
booking_id=booking_id,
|
||||
@@ -100,16 +127,7 @@ async def create_invoice(
|
||||
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"),
|
||||
**invoice_kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import aiofiles
|
||||
import uuid
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/page-content", tags=["page-content"])
|
||||
|
||||
|
||||
@@ -40,12 +47,60 @@ async def get_all_page_contents(
|
||||
"social_links": json.loads(content.social_links) if content.social_links else None,
|
||||
"footer_links": json.loads(content.footer_links) if content.footer_links else None,
|
||||
"badges": json.loads(content.badges) if content.badges else None,
|
||||
"copyright_text": content.copyright_text,
|
||||
"hero_title": content.hero_title,
|
||||
"hero_subtitle": content.hero_subtitle,
|
||||
"hero_image": content.hero_image,
|
||||
"story_content": content.story_content,
|
||||
"values": json.loads(content.values) if content.values else None,
|
||||
"features": json.loads(content.features) if content.features else None,
|
||||
"about_hero_image": content.about_hero_image,
|
||||
"mission": content.mission,
|
||||
"vision": content.vision,
|
||||
"team": json.loads(content.team) if content.team else None,
|
||||
"timeline": json.loads(content.timeline) if content.timeline else None,
|
||||
"achievements": json.loads(content.achievements) if content.achievements else None,
|
||||
"amenities_section_title": content.amenities_section_title,
|
||||
"amenities_section_subtitle": content.amenities_section_subtitle,
|
||||
"amenities": json.loads(content.amenities) if content.amenities else None,
|
||||
"testimonials_section_title": content.testimonials_section_title,
|
||||
"testimonials_section_subtitle": content.testimonials_section_subtitle,
|
||||
"testimonials": json.loads(content.testimonials) if content.testimonials else None,
|
||||
"gallery_section_title": content.gallery_section_title,
|
||||
"gallery_section_subtitle": content.gallery_section_subtitle,
|
||||
"gallery_images": json.loads(content.gallery_images) if content.gallery_images else None,
|
||||
"luxury_section_title": content.luxury_section_title,
|
||||
"luxury_section_subtitle": content.luxury_section_subtitle,
|
||||
"luxury_section_image": content.luxury_section_image,
|
||||
"luxury_features": json.loads(content.luxury_features) if content.luxury_features else None,
|
||||
"luxury_gallery_section_title": content.luxury_gallery_section_title,
|
||||
"luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle,
|
||||
"luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None,
|
||||
"luxury_testimonials_section_title": content.luxury_testimonials_section_title,
|
||||
"luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle,
|
||||
"luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None,
|
||||
"about_preview_title": content.about_preview_title,
|
||||
"about_preview_subtitle": content.about_preview_subtitle,
|
||||
"about_preview_content": content.about_preview_content,
|
||||
"about_preview_image": content.about_preview_image,
|
||||
"stats": json.loads(content.stats) if content.stats else None,
|
||||
"luxury_services_section_title": content.luxury_services_section_title,
|
||||
"luxury_services_section_subtitle": content.luxury_services_section_subtitle,
|
||||
"luxury_services": json.loads(content.luxury_services) if content.luxury_services else None,
|
||||
"luxury_experiences_section_title": content.luxury_experiences_section_title,
|
||||
"luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle,
|
||||
"luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None,
|
||||
"awards_section_title": content.awards_section_title,
|
||||
"awards_section_subtitle": content.awards_section_subtitle,
|
||||
"awards": json.loads(content.awards) if content.awards else None,
|
||||
"cta_title": content.cta_title,
|
||||
"cta_subtitle": content.cta_subtitle,
|
||||
"cta_button_text": content.cta_button_text,
|
||||
"cta_button_link": content.cta_button_link,
|
||||
"cta_image": content.cta_image,
|
||||
"partners_section_title": content.partners_section_title,
|
||||
"partners_section_subtitle": content.partners_section_subtitle,
|
||||
"partners": json.loads(content.partners) if content.partners else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
@@ -65,6 +120,104 @@ async def get_all_page_contents(
|
||||
)
|
||||
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
"""Get base URL for image normalization"""
|
||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}"
|
||||
|
||||
|
||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||
"""Normalize image URL to absolute URL"""
|
||||
if not image_url:
|
||||
return image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
return image_url
|
||||
if image_url.startswith('/'):
|
||||
return f"{base_url}{image_url}"
|
||||
return f"{base_url}/{image_url}"
|
||||
|
||||
|
||||
@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def upload_page_content_image(
|
||||
request: Request,
|
||||
image: UploadFile = File(...),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
):
|
||||
"""Upload page content image (Admin only)"""
|
||||
try:
|
||||
logger.info(f"Upload request received: filename={image.filename}, content_type={image.content_type}")
|
||||
|
||||
# Validate file exists
|
||||
if not image:
|
||||
logger.error("No file provided in upload request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No file provided"
|
||||
)
|
||||
|
||||
# Validate file type
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
logger.error(f"Invalid file type: {image.content_type}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File must be an image. Received: {image.content_type}"
|
||||
)
|
||||
|
||||
# Validate filename
|
||||
if not image.filename:
|
||||
logger.error("No filename provided in upload request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "page-content"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Upload directory: {upload_dir}")
|
||||
|
||||
# Generate filename
|
||||
ext = Path(image.filename).suffix or '.jpg'
|
||||
filename = f"page-content-{uuid.uuid4()}{ext}"
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await image.read()
|
||||
if not content:
|
||||
logger.error("Empty file uploaded")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File is empty"
|
||||
)
|
||||
await f.write(content)
|
||||
logger.info(f"File saved successfully: {file_path}, size: {len(content)} bytes")
|
||||
|
||||
# Return the image URL
|
||||
image_url = f"/uploads/page-content/{filename}"
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
logger.info(f"Upload successful: {image_url}")
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Image uploaded successfully",
|
||||
"data": {
|
||||
"image_url": image_url,
|
||||
"full_url": full_url
|
||||
}
|
||||
}
|
||||
except HTTPException as e:
|
||||
logger.error(f"HTTPException in upload: {e.detail}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error uploading image: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error uploading image: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{page_type}")
|
||||
async def get_page_content(
|
||||
page_type: PageType,
|
||||
@@ -102,12 +255,60 @@ async def get_page_content(
|
||||
"social_links": json.loads(content.social_links) if content.social_links else None,
|
||||
"footer_links": json.loads(content.footer_links) if content.footer_links else None,
|
||||
"badges": json.loads(content.badges) if content.badges else None,
|
||||
"copyright_text": content.copyright_text,
|
||||
"hero_title": content.hero_title,
|
||||
"hero_subtitle": content.hero_subtitle,
|
||||
"hero_image": content.hero_image,
|
||||
"story_content": content.story_content,
|
||||
"values": json.loads(content.values) if content.values else None,
|
||||
"features": json.loads(content.features) if content.features else None,
|
||||
"about_hero_image": content.about_hero_image,
|
||||
"mission": content.mission,
|
||||
"vision": content.vision,
|
||||
"team": json.loads(content.team) if content.team else None,
|
||||
"timeline": json.loads(content.timeline) if content.timeline else None,
|
||||
"achievements": json.loads(content.achievements) if content.achievements else None,
|
||||
"amenities_section_title": content.amenities_section_title,
|
||||
"amenities_section_subtitle": content.amenities_section_subtitle,
|
||||
"amenities": json.loads(content.amenities) if content.amenities else None,
|
||||
"testimonials_section_title": content.testimonials_section_title,
|
||||
"testimonials_section_subtitle": content.testimonials_section_subtitle,
|
||||
"testimonials": json.loads(content.testimonials) if content.testimonials else None,
|
||||
"gallery_section_title": content.gallery_section_title,
|
||||
"gallery_section_subtitle": content.gallery_section_subtitle,
|
||||
"gallery_images": json.loads(content.gallery_images) if content.gallery_images else None,
|
||||
"luxury_section_title": content.luxury_section_title,
|
||||
"luxury_section_subtitle": content.luxury_section_subtitle,
|
||||
"luxury_section_image": content.luxury_section_image,
|
||||
"luxury_features": json.loads(content.luxury_features) if content.luxury_features else None,
|
||||
"luxury_gallery_section_title": content.luxury_gallery_section_title,
|
||||
"luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle,
|
||||
"luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None,
|
||||
"luxury_testimonials_section_title": content.luxury_testimonials_section_title,
|
||||
"luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle,
|
||||
"luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None,
|
||||
"about_preview_title": content.about_preview_title,
|
||||
"about_preview_subtitle": content.about_preview_subtitle,
|
||||
"about_preview_content": content.about_preview_content,
|
||||
"about_preview_image": content.about_preview_image,
|
||||
"stats": json.loads(content.stats) if content.stats else None,
|
||||
"luxury_services_section_title": content.luxury_services_section_title,
|
||||
"luxury_services_section_subtitle": content.luxury_services_section_subtitle,
|
||||
"luxury_services": json.loads(content.luxury_services) if content.luxury_services else None,
|
||||
"luxury_experiences_section_title": content.luxury_experiences_section_title,
|
||||
"luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle,
|
||||
"luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None,
|
||||
"awards_section_title": content.awards_section_title,
|
||||
"awards_section_subtitle": content.awards_section_subtitle,
|
||||
"awards": json.loads(content.awards) if content.awards else None,
|
||||
"cta_title": content.cta_title,
|
||||
"cta_subtitle": content.cta_subtitle,
|
||||
"cta_button_text": content.cta_button_text,
|
||||
"cta_button_link": content.cta_button_link,
|
||||
"cta_image": content.cta_image,
|
||||
"partners_section_title": content.partners_section_title,
|
||||
"partners_section_subtitle": content.partners_section_subtitle,
|
||||
"partners": json.loads(content.partners) if content.partners else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
@@ -151,6 +352,12 @@ async def create_or_update_page_content(
|
||||
story_content: Optional[str] = None,
|
||||
values: Optional[str] = None,
|
||||
features: Optional[str] = None,
|
||||
about_hero_image: Optional[str] = None,
|
||||
mission: Optional[str] = None,
|
||||
vision: Optional[str] = None,
|
||||
team: Optional[str] = None,
|
||||
timeline: Optional[str] = None,
|
||||
achievements: Optional[str] = None,
|
||||
is_active: bool = True,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
@@ -263,6 +470,18 @@ async def create_or_update_page_content(
|
||||
existing_content.values = values
|
||||
if features is not None:
|
||||
existing_content.features = features
|
||||
if about_hero_image is not None:
|
||||
existing_content.about_hero_image = about_hero_image
|
||||
if mission is not None:
|
||||
existing_content.mission = mission
|
||||
if vision is not None:
|
||||
existing_content.vision = vision
|
||||
if team is not None:
|
||||
existing_content.team = team
|
||||
if timeline is not None:
|
||||
existing_content.timeline = timeline
|
||||
if achievements is not None:
|
||||
existing_content.achievements = achievements
|
||||
if is_active is not None:
|
||||
existing_content.is_active = is_active
|
||||
|
||||
@@ -308,6 +527,12 @@ async def create_or_update_page_content(
|
||||
story_content=story_content,
|
||||
values=values,
|
||||
features=features,
|
||||
about_hero_image=about_hero_image,
|
||||
mission=mission,
|
||||
vision=vision,
|
||||
team=team,
|
||||
timeline=timeline,
|
||||
achievements=achievements,
|
||||
is_active=is_active,
|
||||
)
|
||||
|
||||
@@ -362,7 +587,10 @@ async def update_page_content(
|
||||
for key, value in page_data.items():
|
||||
if hasattr(existing_content, key):
|
||||
# Handle JSON fields - convert dict/list to JSON string
|
||||
if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features"] and value is not None:
|
||||
if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features",
|
||||
"amenities", "testimonials", "gallery_images", "stats", "luxury_features",
|
||||
"luxury_gallery", "luxury_testimonials", "luxury_services", "luxury_experiences",
|
||||
"awards", "partners", "team", "timeline", "achievements"] and value is not None:
|
||||
if isinstance(value, str):
|
||||
# Already a string, validate it's valid JSON
|
||||
try:
|
||||
@@ -403,12 +631,60 @@ async def update_page_content(
|
||||
"social_links": json.loads(existing_content.social_links) if existing_content.social_links else None,
|
||||
"footer_links": json.loads(existing_content.footer_links) if existing_content.footer_links else None,
|
||||
"badges": json.loads(existing_content.badges) if existing_content.badges else None,
|
||||
"copyright_text": existing_content.copyright_text,
|
||||
"hero_title": existing_content.hero_title,
|
||||
"hero_subtitle": existing_content.hero_subtitle,
|
||||
"hero_image": existing_content.hero_image,
|
||||
"story_content": existing_content.story_content,
|
||||
"values": json.loads(existing_content.values) if existing_content.values else None,
|
||||
"features": json.loads(existing_content.features) if existing_content.features else None,
|
||||
"about_hero_image": existing_content.about_hero_image,
|
||||
"mission": existing_content.mission,
|
||||
"vision": existing_content.vision,
|
||||
"team": json.loads(existing_content.team) if existing_content.team else None,
|
||||
"timeline": json.loads(existing_content.timeline) if existing_content.timeline else None,
|
||||
"achievements": json.loads(existing_content.achievements) if existing_content.achievements else None,
|
||||
"amenities_section_title": existing_content.amenities_section_title,
|
||||
"amenities_section_subtitle": existing_content.amenities_section_subtitle,
|
||||
"amenities": json.loads(existing_content.amenities) if existing_content.amenities else None,
|
||||
"testimonials_section_title": existing_content.testimonials_section_title,
|
||||
"testimonials_section_subtitle": existing_content.testimonials_section_subtitle,
|
||||
"testimonials": json.loads(existing_content.testimonials) if existing_content.testimonials else None,
|
||||
"gallery_section_title": existing_content.gallery_section_title,
|
||||
"gallery_section_subtitle": existing_content.gallery_section_subtitle,
|
||||
"gallery_images": json.loads(existing_content.gallery_images) if existing_content.gallery_images else None,
|
||||
"luxury_section_title": existing_content.luxury_section_title,
|
||||
"luxury_section_subtitle": existing_content.luxury_section_subtitle,
|
||||
"luxury_section_image": existing_content.luxury_section_image,
|
||||
"luxury_features": json.loads(existing_content.luxury_features) if existing_content.luxury_features else None,
|
||||
"luxury_gallery_section_title": existing_content.luxury_gallery_section_title,
|
||||
"luxury_gallery_section_subtitle": existing_content.luxury_gallery_section_subtitle,
|
||||
"luxury_gallery": json.loads(existing_content.luxury_gallery) if existing_content.luxury_gallery else None,
|
||||
"luxury_testimonials_section_title": existing_content.luxury_testimonials_section_title,
|
||||
"luxury_testimonials_section_subtitle": existing_content.luxury_testimonials_section_subtitle,
|
||||
"luxury_testimonials": json.loads(existing_content.luxury_testimonials) if existing_content.luxury_testimonials else None,
|
||||
"about_preview_title": existing_content.about_preview_title,
|
||||
"about_preview_subtitle": existing_content.about_preview_subtitle,
|
||||
"about_preview_content": existing_content.about_preview_content,
|
||||
"about_preview_image": existing_content.about_preview_image,
|
||||
"stats": json.loads(existing_content.stats) if existing_content.stats else None,
|
||||
"luxury_services_section_title": existing_content.luxury_services_section_title,
|
||||
"luxury_services_section_subtitle": existing_content.luxury_services_section_subtitle,
|
||||
"luxury_services": json.loads(existing_content.luxury_services) if existing_content.luxury_services else None,
|
||||
"luxury_experiences_section_title": existing_content.luxury_experiences_section_title,
|
||||
"luxury_experiences_section_subtitle": existing_content.luxury_experiences_section_subtitle,
|
||||
"luxury_experiences": json.loads(existing_content.luxury_experiences) if existing_content.luxury_experiences else None,
|
||||
"awards_section_title": existing_content.awards_section_title,
|
||||
"awards_section_subtitle": existing_content.awards_section_subtitle,
|
||||
"awards": json.loads(existing_content.awards) if existing_content.awards else None,
|
||||
"cta_title": existing_content.cta_title,
|
||||
"cta_subtitle": existing_content.cta_subtitle,
|
||||
"cta_button_text": existing_content.cta_button_text,
|
||||
"cta_button_link": existing_content.cta_button_link,
|
||||
"cta_image": existing_content.cta_image,
|
||||
"partners_section_title": existing_content.partners_section_title,
|
||||
"partners_section_subtitle": existing_content.partners_section_subtitle,
|
||||
"partners": json.loads(existing_content.partners) if existing_content.partners else None,
|
||||
"is_active": existing_content.is_active,
|
||||
"updated_at": existing_content.updated_at.isoformat() if existing_content.updated_at else None,
|
||||
}
|
||||
|
||||
@@ -694,18 +694,24 @@ async def upload_room_images(
|
||||
image_urls = []
|
||||
for image in images:
|
||||
# Validate file type
|
||||
if not image.content_type.startswith('image/'):
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
continue
|
||||
|
||||
# Validate filename
|
||||
if not image.filename:
|
||||
continue
|
||||
|
||||
# Generate filename
|
||||
import uuid
|
||||
ext = Path(image.filename).suffix
|
||||
ext = Path(image.filename).suffix or '.jpg'
|
||||
filename = f"room-{uuid.uuid4()}{ext}"
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await image.read()
|
||||
if not content:
|
||||
continue
|
||||
await f.write(content)
|
||||
|
||||
image_urls.append(f"/uploads/rooms/{filename}")
|
||||
@@ -717,6 +723,7 @@ async def upload_room_images(
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Images uploaded successfully",
|
||||
"data": {"images": updated_images}
|
||||
|
||||
@@ -1127,6 +1127,7 @@ async def upload_company_logo(
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Logo uploaded successfully",
|
||||
"data": {
|
||||
@@ -1235,6 +1236,7 @@ async def upload_company_favicon(
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Favicon uploaded successfully",
|
||||
"data": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -146,16 +146,28 @@ class AuthService:
|
||||
|
||||
async def login(self, db: Session, email: str, password: str, remember_me: bool = False, mfa_token: str = None) -> dict:
|
||||
"""Login user with optional MFA verification"""
|
||||
# Normalize email (lowercase and strip whitespace)
|
||||
email = email.lower().strip() if email else ""
|
||||
if not email:
|
||||
raise ValueError("Invalid email or password")
|
||||
|
||||
# Find user with role and password
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
if not user:
|
||||
logger.warning(f"Login attempt with non-existent email: {email}")
|
||||
raise ValueError("Invalid email or password")
|
||||
|
||||
# Check if user is active
|
||||
if not user.is_active:
|
||||
logger.warning(f"Login attempt for inactive user: {email}")
|
||||
raise ValueError("Account is disabled. Please contact support.")
|
||||
|
||||
# Load role
|
||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||
|
||||
# Check password
|
||||
if not self.verify_password(password, user.password):
|
||||
logger.warning(f"Login attempt with invalid password for user: {email}")
|
||||
raise ValueError("Invalid email or password")
|
||||
|
||||
# Check if MFA is enabled
|
||||
|
||||
@@ -87,10 +87,20 @@ class InvoiceService:
|
||||
|
||||
# Calculate amounts - subtotal will be recalculated after adding items
|
||||
# Initial subtotal is booking total (room + services) or invoice_amount if specified
|
||||
booking_total = float(booking.total_price)
|
||||
if invoice_amount is not None:
|
||||
subtotal = float(invoice_amount)
|
||||
# For partial invoices, ensure discount is proportional
|
||||
# If discount_amount seems too large (greater than subtotal), recalculate proportionally
|
||||
if invoice_amount < booking_total and discount_amount > 0:
|
||||
# Check if discount seems disproportionate (greater than 50% of subtotal suggests it's the full discount)
|
||||
if discount_amount > subtotal * 0.5:
|
||||
# Recalculate proportionally from booking's original discount
|
||||
proportion = float(invoice_amount) / booking_total
|
||||
original_discount = float(booking.discount_amount) if booking.discount_amount else discount_amount
|
||||
discount_amount = original_discount * proportion
|
||||
else:
|
||||
subtotal = float(booking.total_price)
|
||||
subtotal = booking_total
|
||||
|
||||
# Calculate tax and total amounts
|
||||
tax_amount = (subtotal - discount_amount) * (tax_rate / 100)
|
||||
@@ -388,6 +398,14 @@ class InvoiceService:
|
||||
@staticmethod
|
||||
def invoice_to_dict(invoice: Invoice) -> Dict[str, Any]:
|
||||
"""Convert invoice model to dictionary"""
|
||||
# Extract promotion code from notes if present
|
||||
promotion_code = None
|
||||
if invoice.notes and "Promotion Code:" in invoice.notes:
|
||||
try:
|
||||
promotion_code = invoice.notes.split("Promotion Code:")[1].split("\n")[0].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"id": invoice.id,
|
||||
"invoice_number": invoice.invoice_number,
|
||||
@@ -419,6 +437,7 @@ class InvoiceService:
|
||||
"terms_and_conditions": invoice.terms_and_conditions,
|
||||
"payment_instructions": invoice.payment_instructions,
|
||||
"is_proforma": invoice.is_proforma if hasattr(invoice, 'is_proforma') else False,
|
||||
"promotion_code": promotion_code,
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
|
||||
@@ -505,10 +505,36 @@ class PayPalService:
|
||||
except Exception as email_error:
|
||||
logger.error(f"Failed to send booking confirmation email: {str(email_error)}")
|
||||
|
||||
# Send invoice email if payment is completed and invoice is now paid
|
||||
from ..utils.mailer import send_email
|
||||
from ..services.invoice_service import InvoiceService
|
||||
from ..routes.booking_routes import _generate_invoice_email_html
|
||||
|
||||
# Load user for email
|
||||
from ..models.user import User
|
||||
user = db.query(User).filter(User.id == booking.user_id).first()
|
||||
|
||||
for invoice in invoices:
|
||||
if invoice.status == InvoiceStatus.paid and invoice.balance_due <= 0:
|
||||
try:
|
||||
invoice_dict = InvoiceService.invoice_to_dict(invoice)
|
||||
invoice_html = _generate_invoice_email_html(invoice_dict, is_proforma=invoice.is_proforma)
|
||||
invoice_type = "Proforma Invoice" if invoice.is_proforma else "Invoice"
|
||||
if user:
|
||||
await send_email(
|
||||
to=user.email,
|
||||
subject=f"{invoice_type} {invoice.invoice_number} - Payment Confirmed",
|
||||
html=invoice_html
|
||||
)
|
||||
logger.info(f"{invoice_type} {invoice.invoice_number} sent to {user.email}")
|
||||
except Exception as email_error:
|
||||
logger.error(f"Failed to send invoice email: {str(email_error)}")
|
||||
|
||||
# Send invoice email if payment is completed and invoice is now paid
|
||||
from ..utils.mailer import send_email
|
||||
from ..services.invoice_service import InvoiceService
|
||||
from ..models.invoice import InvoiceStatus
|
||||
from ..routes.booking_routes import _generate_invoice_email_html
|
||||
|
||||
# Load user for email
|
||||
from ..models.user import User
|
||||
|
||||
@@ -404,6 +404,7 @@ class StripeService:
|
||||
# Send invoice email if payment is completed and invoice is now paid
|
||||
from ..utils.mailer import send_email
|
||||
from ..services.invoice_service import InvoiceService
|
||||
from ..routes.booking_routes import _generate_invoice_email_html
|
||||
|
||||
# Load user for email
|
||||
from ..models.user import User
|
||||
|
||||
Reference in New Issue
Block a user