updates
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
"""
|
||||
Email templates for various notifications
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from ..config.database import SessionLocal
|
||||
from ..models.system_settings import SystemSettings
|
||||
|
||||
|
||||
def _get_company_settings():
|
||||
"""Get company settings from database"""
|
||||
try:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -44,9 +39,7 @@ def _get_company_settings():
|
||||
"company_address": None,
|
||||
}
|
||||
|
||||
|
||||
def get_base_template(content: str, title: str = "Hotel Booking", client_url: str = "http://localhost:5173") -> str:
|
||||
"""Luxury HTML email template with premium company branding"""
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
company_tagline = company_settings.get("company_tagline") or "Excellence Redefined"
|
||||
@@ -55,12 +48,12 @@ def get_base_template(content: str, title: str = "Hotel Booking", client_url: st
|
||||
company_email = company_settings.get("company_email")
|
||||
company_address = company_settings.get("company_address")
|
||||
|
||||
# Build logo HTML if logo exists
|
||||
|
||||
logo_html = ""
|
||||
if company_logo_url:
|
||||
# Convert relative URL to absolute if needed
|
||||
|
||||
if not company_logo_url.startswith('http'):
|
||||
# Try to construct full URL
|
||||
|
||||
server_url = client_url.replace('://localhost:5173', '').replace('://localhost:3000', '')
|
||||
if not server_url.startswith('http'):
|
||||
server_url = f"http://{server_url}" if ':' not in server_url.split('//')[-1] else server_url
|
||||
@@ -68,187 +61,45 @@ def get_base_template(content: str, title: str = "Hotel Booking", client_url: st
|
||||
else:
|
||||
full_logo_url = company_logo_url
|
||||
|
||||
logo_html = f'''
|
||||
<div style="text-align: center; padding: 15px 0;">
|
||||
<img src="{full_logo_url}" alt="{company_name}" style="max-height: 80px; max-width: 280px; margin: 0 auto; display: block; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1));" />
|
||||
{f'<p style="color: #D4AF37; margin: 8px 0 0 0; font-size: 11px; letter-spacing: 2px; text-transform: uppercase; font-weight: 300;">{company_tagline}</p>' if company_tagline else ''}
|
||||
</div>
|
||||
'''
|
||||
logo_html = f
|
||||
else:
|
||||
logo_html = f'''
|
||||
<div style="text-align: center; padding: 15px 0;">
|
||||
<h1 style="color: #ffffff; margin: 0; font-size: 32px; font-weight: 600; letter-spacing: 1px;">{company_name}</h1>
|
||||
{f'<p style="color: #D4AF37; margin: 8px 0 0 0; font-size: 11px; letter-spacing: 2px; text-transform: uppercase; font-weight: 300;">{company_tagline}</p>' if company_tagline else ''}
|
||||
</div>
|
||||
'''
|
||||
logo_html = f
|
||||
|
||||
# Build footer contact info
|
||||
|
||||
footer_contact = ""
|
||||
if company_phone or company_email or company_address:
|
||||
footer_contact = '''
|
||||
<div style="margin-top: 25px; padding-top: 25px; border-top: 1px solid rgba(212, 175, 55, 0.2);">
|
||||
<table role="presentation" style="width: 100%; max-width: 500px; margin: 0 auto;">
|
||||
'''
|
||||
footer_contact =
|
||||
if company_phone:
|
||||
footer_contact += f'''
|
||||
<tr>
|
||||
<td style="padding: 5px 0; text-align: center;">
|
||||
<p style="margin: 0; color: #999999; font-size: 13px;">📞 {company_phone}</p>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
footer_contact += f
|
||||
if company_email:
|
||||
footer_contact += f'''
|
||||
<tr>
|
||||
<td style="padding: 5px 0; text-align: center;">
|
||||
<p style="margin: 0; color: #999999; font-size: 13px;">✉️ <a href="mailto:{company_email}" style="color: #D4AF37; text-decoration: none;">{company_email}</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
footer_contact += f
|
||||
if company_address:
|
||||
# Replace newlines with <br> for address
|
||||
formatted_address = company_address.replace('\n', '<br>')
|
||||
footer_contact += f'''
|
||||
<tr>
|
||||
<td style="padding: 5px 0; text-align: center;">
|
||||
<p style="margin: 0; color: #999999; font-size: 13px; line-height: 1.6;">📍 {formatted_address}</p>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
footer_contact += '''
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap');
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); background-color: #1a1a1a;">
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse; background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);">
|
||||
<tr>
|
||||
<td style="padding: 40px 20px; background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%); background-color: #1a1a1a; border-bottom: 2px solid rgba(212, 175, 55, 0.3);">
|
||||
{logo_html}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<table role="presentation" style="width: 100%; max-width: 650px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
|
||||
<tr>
|
||||
<td style="padding: 50px 40px; background: linear-gradient(to bottom, #ffffff 0%, #fafafa 100%);">
|
||||
{content}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 40px 20px; text-align: center; background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); background-color: #1a1a1a; color: #999999; font-size: 12px;">
|
||||
<p style="margin: 0 0 15px 0; color: #666666; font-size: 12px;">This is an automated email. Please do not reply.</p>
|
||||
<p style="margin: 0 0 20px 0; color: #D4AF37; font-size: 13px; font-weight: 500;">© {datetime.now().year} {company_name}. All rights reserved.</p>
|
||||
{footer_contact}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
formatted_address = company_address.replace('\n', '<br>')
|
||||
footer_contact += f
|
||||
footer_contact +=
|
||||
|
||||
return f
|
||||
|
||||
def welcome_email_template(name: str, email: str, client_url: str) -> str:
|
||||
"""Welcome email template for new registrations"""
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">✨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 15px; font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; text-align: center; letter-spacing: -0.5px;">Welcome, {name}!</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 25px;">We are delighted to welcome you to <strong style="color: #1a1a1a;">{company_name}</strong>.</p>
|
||||
<p style="color: #666666; font-size: 15px; line-height: 1.7; text-align: center; margin-bottom: 30px;">Your account has been successfully created with email: <strong style="color: #D4AF37;">{email}</strong></p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #fef9e7 0%, #fdf6e3 100%); border-left: 4px solid #D4AF37; padding: 25px; border-radius: 10px; margin: 30px 0; box-shadow: 0 4px 15px rgba(212, 175, 55, 0.1);">
|
||||
<p style="margin: 0 0 15px 0; color: #1a1a1a; font-weight: 600; font-size: 16px;">🎁 What you can do:</p>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #555555; line-height: 2;">
|
||||
<li style="margin-bottom: 8px;">Search and book our exquisite hotel rooms</li>
|
||||
<li style="margin-bottom: 8px;">Manage your bookings with ease</li>
|
||||
<li style="margin-bottom: 8px;">Update your personal information anytime</li>
|
||||
<li>Enjoy exclusive member benefits and offers</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<a href="{client_url}/login" style="background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); color: #1a1a1a; padding: 16px 40px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: 600; font-size: 15px; letter-spacing: 0.5px; box-shadow: 0 6px 20px rgba(212, 175, 55, 0.4); transition: all 0.3s ease;">
|
||||
Access Your Account
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
return get_base_template(content, f"Welcome to {company_name}", client_url)
|
||||
|
||||
|
||||
def password_reset_email_template(reset_url: str) -> str:
|
||||
"""Password reset email template"""
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">🔐</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 15px; font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 700; text-align: center;">Password Reset Request</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 20px;">A password reset request has been received for your account.</p>
|
||||
<p style="color: #666666; font-size: 15px; line-height: 1.7; text-align: center; margin-bottom: 30px;">Click the button below to reset your password. <strong style="color: #D4AF37;">This link will expire in 1 hour.</strong></p>
|
||||
|
||||
<div style="text-align: center; margin: 35px 0;">
|
||||
<a href="{reset_url}" style="background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); color: #1a1a1a; padding: 16px 40px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: 600; font-size: 15px; letter-spacing: 0.5px; box-shadow: 0 6px 20px rgba(212, 175, 55, 0.4);">
|
||||
Reset Password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff9e6; border: 1px solid #ffe0b2; padding: 15px; border-radius: 8px; margin-top: 30px;">
|
||||
<p style="margin: 0; color: #856404; font-size: 13px; text-align: center; line-height: 1.6;">⚠️ If you did not request this password reset, please ignore this email and your password will remain unchanged.</p>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
return get_base_template(content, f"Password Reset - {company_name}", reset_url.split('/reset-password')[0] if '/reset-password' in reset_url else "http://localhost:5173")
|
||||
|
||||
|
||||
def password_changed_email_template(email: str) -> str:
|
||||
"""Password changed confirmation email template"""
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, #10B981 0%, #059669 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 15px; font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 700; text-align: center;">Password Changed Successfully</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 25px;">The password for account <strong style="color: #1a1a1a;">{email}</strong> has been changed successfully.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); border-left: 4px solid #10B981; padding: 20px; border-radius: 10px; margin: 30px 0; box-shadow: 0 4px 15px rgba(16, 185, 129, 0.1);">
|
||||
<p style="margin: 0; color: #065F46; font-size: 14px; line-height: 1.6; text-align: center;">🔒 If you did not make this change, please contact our support team immediately to secure your account.</p>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
return get_base_template(content, f"Password Changed - {company_name}", "http://localhost:5173")
|
||||
|
||||
|
||||
def booking_confirmation_email_template(
|
||||
booking_number: str,
|
||||
guest_name: str,
|
||||
@@ -268,125 +119,27 @@ def booking_confirmation_email_template(
|
||||
client_url: str = "http://localhost:5173",
|
||||
currency_symbol: str = "$"
|
||||
) -> str:
|
||||
"""Booking confirmation email template"""
|
||||
deposit_info = ""
|
||||
if requires_deposit and deposit_amount and amount_paid is None:
|
||||
deposit_info = f"""
|
||||
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border-left: 4px solid #F59E0B; padding: 25px; margin: 30px 0; border-radius: 10px; box-shadow: 0 4px 15px rgba(245, 158, 11, 0.2);">
|
||||
<p style="margin: 0 0 10px 0; font-weight: 700; color: #92400E; font-size: 16px;">⚠️ Deposit Required</p>
|
||||
<p style="margin: 0 0 8px 0; color: #78350F; font-size: 15px; line-height: 1.6;">Please pay a deposit of <strong style="color: #92400E; font-size: 18px;">{currency_symbol}{deposit_amount:.2f}</strong> to confirm your booking.</p>
|
||||
<p style="margin: 0; color: #78350F; font-size: 14px;">Your booking will be confirmed once the deposit is received.</p>
|
||||
</div>
|
||||
"""
|
||||
deposit_info = f
|
||||
|
||||
# Payment breakdown section (shown when payment is completed)
|
||||
|
||||
payment_breakdown = ""
|
||||
if amount_paid is not None:
|
||||
remaining_due = total_price - amount_paid
|
||||
payment_type_label = "Deposit Payment" if payment_type == "deposit" else "Full Payment"
|
||||
payment_breakdown = f"""
|
||||
<div style="background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); border-left: 4px solid #10B981; padding: 25px; margin: 30px 0; border-radius: 10px; box-shadow: 0 4px 15px rgba(16, 185, 129, 0.2);">
|
||||
<h3 style="margin-top: 0; margin-bottom: 20px; color: #065F46; font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600;">Payment Information</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #065F46; font-size: 14px; font-weight: 500;">Payment Type:</td>
|
||||
<td style="padding: 10px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{payment_type_label}</td>
|
||||
</tr>
|
||||
<tr style="background-color: rgba(16, 185, 129, 0.1);">
|
||||
<td style="padding: 10px 0; color: #065F46; font-size: 14px; font-weight: 500;">Amount Paid:</td>
|
||||
<td style="padding: 10px 0; font-weight: 700; color: #059669; font-size: 18px;">{currency_symbol}{amount_paid:.2f}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #065F46; font-size: 14px; font-weight: 500;">Total Booking Price:</td>
|
||||
<td style="padding: 10px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{currency_symbol}{total_price:.2f}</td>
|
||||
</tr>
|
||||
"""
|
||||
payment_breakdown = f
|
||||
if remaining_due > 0:
|
||||
payment_breakdown += f"""
|
||||
<tr style="background-color: rgba(245, 158, 11, 0.1); border-top: 2px solid #F59E0B;">
|
||||
<td style="padding: 10px 0; color: #92400E; font-size: 14px; font-weight: 600;">Remaining Due:</td>
|
||||
<td style="padding: 10px 0; font-weight: 700; color: #B45309; font-size: 18px;">{currency_symbol}{remaining_due:.2f}</td>
|
||||
</tr>
|
||||
"""
|
||||
payment_breakdown += f
|
||||
else:
|
||||
payment_breakdown += f"""
|
||||
<tr style="background-color: rgba(16, 185, 129, 0.1); border-top: 2px solid #10B981;">
|
||||
<td style="padding: 10px 0; color: #065F46; font-size: 14px; font-weight: 600;">Status:</td>
|
||||
<td style="padding: 10px 0; font-weight: 700; color: #059669; font-size: 16px;">✅ Fully Paid</td>
|
||||
</tr>
|
||||
"""
|
||||
payment_breakdown += """
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
payment_breakdown += f
|
||||
payment_breakdown +=
|
||||
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">🏨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 20px; font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; text-align: center;">Booking Confirmation</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 10px;">Dear <strong style="color: #1a1a1a;">{guest_name}</strong>,</p>
|
||||
<p style="color: #666666; font-size: 15px; line-height: 1.7; text-align: center; margin-bottom: 35px;">Thank you for choosing us! We have received your reservation request and are delighted to welcome you.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%); padding: 30px; border-radius: 12px; margin: 30px 0; border: 1px solid #e5e5e5; box-shadow: 0 4px 20px rgba(0,0,0,0.05);">
|
||||
<h3 style="margin-top: 0; margin-bottom: 25px; color: #1a1a1a; font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 600; text-align: center; border-bottom: 2px solid #D4AF37; padding-bottom: 15px;">Booking Details</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; width: 45%; font-size: 14px; font-weight: 500;">Booking Number:</td>
|
||||
<td style="padding: 12px 0; font-weight: 700; color: #1a1a1a; font-size: 15px; letter-spacing: 0.5px;">{booking_number}</td>
|
||||
</tr>
|
||||
<tr style="background-color: rgba(212, 175, 55, 0.05);">
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Room:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{room_type} - Room {room_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Check-in:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{check_in}</td>
|
||||
</tr>
|
||||
<tr style="background-color: rgba(212, 175, 55, 0.05);">
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Check-out:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{check_out}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Guests:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{num_guests} guest{'s' if num_guests > 1 else ''}</td>
|
||||
</tr>
|
||||
{f'''
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Subtotal:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{currency_symbol}{original_price:.2f}</td>
|
||||
</tr>
|
||||
<tr style="background-color: rgba(16, 185, 129, 0.1);">
|
||||
<td style="padding: 12px 0; color: #065F46; font-size: 14px; font-weight: 500;">Promotion Discount{f' ({promotion_code})' if promotion_code else ''}:</td>
|
||||
<td style="padding: 12px 0; font-weight: 700; color: #059669; font-size: 15px;">-{currency_symbol}{discount_amount:.2f}</td>
|
||||
</tr>
|
||||
''' if original_price and discount_amount and discount_amount > 0 else ''}
|
||||
<tr style="background: linear-gradient(135deg, #fef9e7 0%, #fdf6e3 100%); border-top: 2px solid #D4AF37; border-bottom: 2px solid #D4AF37;">
|
||||
<td style="padding: 15px 0; color: #1a1a1a; font-size: 16px; font-weight: 600;">Total Price:</td>
|
||||
<td style="padding: 15px 0; font-weight: 700; color: #D4AF37; font-size: 22px; font-family: 'Playfair Display', serif;">{currency_symbol}{total_price:.2f}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{payment_breakdown}
|
||||
|
||||
{deposit_info}
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<a href="{client_url}/bookings/{booking_number}" style="background: linear-gradient(135deg, #D4AF37 0%, #C9A227 100%); color: #1a1a1a; padding: 16px 40px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: 600; font-size: 15px; letter-spacing: 0.5px; box-shadow: 0 6px 20px rgba(212, 175, 55, 0.4);">
|
||||
View Booking Details
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
return get_base_template(content, f"Booking Confirmation - {company_name}", client_url)
|
||||
|
||||
|
||||
def payment_confirmation_email_template(
|
||||
booking_number: str,
|
||||
guest_name: str,
|
||||
@@ -398,139 +151,44 @@ def payment_confirmation_email_template(
|
||||
client_url: str = "http://localhost:5173",
|
||||
currency_symbol: str = "$"
|
||||
) -> str:
|
||||
"""Payment confirmation email template"""
|
||||
transaction_info = ""
|
||||
if transaction_id:
|
||||
transaction_info = f"""
|
||||
<tr style="background-color: rgba(16, 185, 129, 0.05);">
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Transaction ID:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-family: 'Courier New', monospace; font-size: 13px; letter-spacing: 0.5px;">{transaction_id}</td>
|
||||
</tr>
|
||||
"""
|
||||
transaction_info = f
|
||||
|
||||
payment_type_info = ""
|
||||
if payment_type:
|
||||
payment_type_label = "Deposit Payment (20%)" if payment_type == "deposit" else "Full Payment"
|
||||
payment_type_info = f"""
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Payment Type:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{payment_type_label}</td>
|
||||
</tr>
|
||||
"""
|
||||
payment_type_info = f
|
||||
|
||||
total_price_info = ""
|
||||
remaining_due_info = ""
|
||||
if total_price is not None:
|
||||
total_price_info = f"""
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Total Booking Price:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{currency_symbol}{total_price:.2f}</td>
|
||||
</tr>
|
||||
"""
|
||||
total_price_info = f
|
||||
if payment_type == "deposit" and total_price > amount:
|
||||
remaining_due = total_price - amount
|
||||
remaining_due_info = f"""
|
||||
<tr style="background-color: rgba(245, 158, 11, 0.1);">
|
||||
<td style="padding: 12px 0; color: #92400E; font-size: 14px; font-weight: 600;">Remaining Due:</td>
|
||||
<td style="padding: 12px 0; font-weight: 700; color: #B45309; font-size: 18px;">{currency_symbol}{remaining_due:.2f}</td>
|
||||
</tr>
|
||||
"""
|
||||
remaining_due_info = f
|
||||
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, #10B981 0%, #059669 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">💳</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 20px; font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; text-align: center;">Payment Received</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 10px;">Dear <strong style="color: #1a1a1a;">{guest_name}</strong>,</p>
|
||||
<p style="color: #666666; font-size: 15px; line-height: 1.7; text-align: center; margin-bottom: 35px;">We have successfully received your payment for booking <strong style="color: #10B981;">{booking_number}</strong>.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); border-left: 4px solid #10B981; padding: 30px; border-radius: 12px; margin: 30px 0; box-shadow: 0 4px 20px rgba(16, 185, 129, 0.15);">
|
||||
<h3 style="margin-top: 0; margin-bottom: 25px; color: #065F46; font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 600; text-align: center; border-bottom: 2px solid #10B981; padding-bottom: 15px;">Payment Details</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; width: 45%; font-size: 14px; font-weight: 500;">Booking Number:</td>
|
||||
<td style="padding: 12px 0; font-weight: 700; color: #1a1a1a; font-size: 15px; letter-spacing: 0.5px;">{booking_number}</td>
|
||||
</tr>
|
||||
<tr style="background-color: rgba(16, 185, 129, 0.1);">
|
||||
<td style="padding: 12px 0; color: #888888; font-size: 14px; font-weight: 500;">Payment Method:</td>
|
||||
<td style="padding: 12px 0; color: #1a1a1a; font-size: 15px; font-weight: 600;">{payment_method}</td>
|
||||
</tr>
|
||||
{transaction_info}
|
||||
{payment_type_info}
|
||||
{total_price_info}
|
||||
<tr style="background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); border-top: 2px solid #10B981; border-bottom: 2px solid #10B981;">
|
||||
<td style="padding: 15px 0; color: #065F46; font-size: 16px; font-weight: 600;">Amount Paid:</td>
|
||||
<td style="padding: 15px 0; font-weight: 700; color: #059669; font-size: 24px; font-family: 'Playfair Display', serif;">{currency_symbol}{amount:.2f}</td>
|
||||
</tr>
|
||||
{remaining_due_info}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin: 35px 0 25px 0;">✨ Your booking is now confirmed. We look forward to hosting you!</p>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<a href="{client_url}/bookings/{booking_number}" style="background: linear-gradient(135deg, #10B981 0%, #059669 100%); color: #ffffff; padding: 16px 40px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: 600; font-size: 15px; letter-spacing: 0.5px; box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);">
|
||||
View Booking
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
return get_base_template(content, f"Payment Confirmation - {company_name}", client_url)
|
||||
|
||||
|
||||
def booking_status_changed_email_template(
|
||||
booking_number: str,
|
||||
guest_name: str,
|
||||
status: str,
|
||||
client_url: str = "http://localhost:5173"
|
||||
) -> str:
|
||||
"""Booking status change email template"""
|
||||
status_colors = {
|
||||
"confirmed": ("#10B981", "Confirmed", "✅", "#ecfdf5", "#d1fae5"),
|
||||
"cancelled": ("#EF4444", "Cancelled", "❌", "#fef2f2", "#fee2e2"),
|
||||
"checked_in": ("#3B82F6", "Checked In", "🔑", "#eff6ff", "#dbeafe"),
|
||||
"checked_out": ("#8B5CF6", "Checked Out", "🏃", "#f5f3ff", "#e9d5ff"),
|
||||
"confirmed": ("
|
||||
"cancelled": ("
|
||||
"checked_in": ("
|
||||
"checked_out": ("
|
||||
}
|
||||
|
||||
color, status_text, icon, bg_start, bg_end = status_colors.get(status.lower(), ("#6B7280", status.title(), "📋", "#f3f4f6", "#e5e7eb"))
|
||||
color, status_text, icon, bg_start, bg_end = status_colors.get(status.lower(), ("
|
||||
|
||||
content = f"""
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: inline-block; background: linear-gradient(135deg, {color} 0%, {color}dd 100%); padding: 3px; border-radius: 50%; margin-bottom: 20px;">
|
||||
<div style="background-color: #ffffff; border-radius: 50%; padding: 15px;">
|
||||
<span style="font-size: 32px;">{icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="color: #1a1a1a; margin-top: 0; margin-bottom: 20px; font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; text-align: center;">Booking Status Updated</h2>
|
||||
<p style="color: #666666; font-size: 16px; line-height: 1.7; text-align: center; margin-bottom: 10px;">Dear <strong style="color: #1a1a1a;">{guest_name}</strong>,</p>
|
||||
<p style="color: #666666; font-size: 15px; line-height: 1.7; text-align: center; margin-bottom: 35px;">Your booking status has been updated.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, {bg_start} 0%, {bg_end} 100%); border-left: 4px solid {color}; padding: 30px; border-radius: 12px; margin: 30px 0; box-shadow: 0 4px 20px {color}20;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 25px; color: {color}; font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 600; text-align: center; border-bottom: 2px solid {color}; padding-bottom: 15px;">Status Information</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 12px 0; color: #888888; width: 45%; font-size: 14px; font-weight: 500;">Booking Number:</td>
|
||||
<td style="padding: 12px 0; font-weight: 700; color: #1a1a1a; font-size: 15px; letter-spacing: 0.5px;">{booking_number}</td>
|
||||
</tr>
|
||||
<tr style="background: linear-gradient(135deg, {bg_end} 0%, {bg_start} 100%); border-top: 2px solid {color}; border-bottom: 2px solid {color};">
|
||||
<td style="padding: 15px 0; color: {color}; font-size: 16px; font-weight: 600;">New Status:</td>
|
||||
<td style="padding: 15px 0; font-weight: 700; color: {color}; font-size: 22px; font-family: 'Playfair Display', serif;">{status_text}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<a href="{client_url}/bookings/{booking_number}" style="background: linear-gradient(135deg, {color} 0%, {color}dd 100%); color: #ffffff; padding: 16px 40px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: 600; font-size: 15px; letter-spacing: 0.5px; box-shadow: 0 6px 20px {color}40;">
|
||||
View Booking
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
content = f
|
||||
company_settings = _get_company_settings()
|
||||
company_name = company_settings.get("company_name") or "Hotel Booking"
|
||||
return get_base_template(content, f"Booking {status_text} - {company_name}", client_url)
|
||||
|
||||
@@ -6,148 +6,86 @@ import logging
|
||||
from ..config.settings import settings
|
||||
from ..config.database import SessionLocal
|
||||
from ..models.system_settings import SystemSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_smtp_settings_from_db():
|
||||
"""
|
||||
Get SMTP settings from system_settings table.
|
||||
Returns dict with settings or None if not available.
|
||||
"""
|
||||
try:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
smtp_settings = {}
|
||||
setting_keys = [
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_user",
|
||||
"smtp_password",
|
||||
"smtp_from_email",
|
||||
"smtp_from_name",
|
||||
"smtp_use_tls",
|
||||
]
|
||||
|
||||
setting_keys = ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'smtp_from_email', 'smtp_from_name', 'smtp_use_tls']
|
||||
for key in setting_keys:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == key
|
||||
).first()
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if setting and setting.value:
|
||||
smtp_settings[key] = setting.value
|
||||
|
||||
# Only return if we have at least host, user, and password
|
||||
if smtp_settings.get("smtp_host") and smtp_settings.get("smtp_user") and smtp_settings.get("smtp_password"):
|
||||
if smtp_settings.get('smtp_host') and smtp_settings.get('smtp_user') and smtp_settings.get('smtp_password'):
|
||||
return smtp_settings
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch SMTP settings from database: {str(e)}")
|
||||
logger.debug(f'Could not fetch SMTP settings from database: {str(e)}')
|
||||
return None
|
||||
|
||||
|
||||
async def send_email(to: str, subject: str, html: str = None, text: str = None):
|
||||
"""
|
||||
Send email using SMTP
|
||||
Uses system_settings first, then falls back to config/settings.py and environment variables
|
||||
"""
|
||||
async def send_email(to: str, subject: str, html: str=None, text: str=None):
|
||||
try:
|
||||
# Try to get SMTP settings from database first
|
||||
db_smtp_settings = _get_smtp_settings_from_db()
|
||||
|
||||
if db_smtp_settings:
|
||||
# Use settings from database
|
||||
mail_host = db_smtp_settings.get("smtp_host")
|
||||
mail_user = db_smtp_settings.get("smtp_user")
|
||||
mail_pass = db_smtp_settings.get("smtp_password")
|
||||
mail_port = int(db_smtp_settings.get("smtp_port", "587"))
|
||||
mail_use_tls = db_smtp_settings.get("smtp_use_tls", "true").lower() == "true"
|
||||
from_address = db_smtp_settings.get("smtp_from_email")
|
||||
from_name = db_smtp_settings.get("smtp_from_name", "Hotel Booking")
|
||||
logger.info("Using SMTP settings from system_settings database")
|
||||
mail_host = db_smtp_settings.get('smtp_host')
|
||||
mail_user = db_smtp_settings.get('smtp_user')
|
||||
mail_pass = db_smtp_settings.get('smtp_password')
|
||||
mail_port = int(db_smtp_settings.get('smtp_port', '587'))
|
||||
mail_use_tls = db_smtp_settings.get('smtp_use_tls', 'true').lower() == 'true'
|
||||
from_address = db_smtp_settings.get('smtp_from_email')
|
||||
from_name = db_smtp_settings.get('smtp_from_name', 'Hotel Booking')
|
||||
logger.info('Using SMTP settings from system_settings database')
|
||||
else:
|
||||
# Fallback to config/settings.py and env vars
|
||||
mail_host = settings.SMTP_HOST or os.getenv("MAIL_HOST")
|
||||
mail_user = settings.SMTP_USER or os.getenv("MAIL_USER")
|
||||
mail_pass = settings.SMTP_PASSWORD or os.getenv("MAIL_PASS")
|
||||
mail_port = settings.SMTP_PORT or int(os.getenv("MAIL_PORT", "587"))
|
||||
mail_secure = os.getenv("MAIL_SECURE", "false").lower() == "true"
|
||||
mail_use_tls = mail_secure # For backward compatibility
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
|
||||
# Get from address - prefer settings, then env, then generate from client_url
|
||||
from_address = settings.SMTP_FROM_EMAIL or os.getenv("MAIL_FROM")
|
||||
mail_host = settings.SMTP_HOST or os.getenv('MAIL_HOST')
|
||||
mail_user = settings.SMTP_USER or os.getenv('MAIL_USER')
|
||||
mail_pass = settings.SMTP_PASSWORD or os.getenv('MAIL_PASS')
|
||||
mail_port = settings.SMTP_PORT or int(os.getenv('MAIL_PORT', '587'))
|
||||
mail_secure = os.getenv('MAIL_SECURE', 'false').lower() == 'true'
|
||||
mail_use_tls = mail_secure
|
||||
client_url = settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
||||
from_address = settings.SMTP_FROM_EMAIL or os.getenv('MAIL_FROM')
|
||||
if not from_address:
|
||||
# Generate from client_url if not set
|
||||
domain = client_url.replace('https://', '').replace('http://', '').split('/')[0]
|
||||
from_address = f"no-reply@{domain}"
|
||||
|
||||
# Use from name if available
|
||||
from_name = settings.SMTP_FROM_NAME or "Hotel Booking"
|
||||
logger.info("Using SMTP settings from config/environment variables")
|
||||
|
||||
from_header = f"{from_name} <{from_address}>"
|
||||
|
||||
from_address = f'no-reply@{domain}'
|
||||
from_name = settings.SMTP_FROM_NAME or 'Hotel Booking'
|
||||
logger.info('Using SMTP settings from config/environment variables')
|
||||
from_header = f'{from_name} <{from_address}>'
|
||||
if not (mail_host and mail_user and mail_pass):
|
||||
error_msg = "SMTP mailer not configured. Set SMTP_HOST, SMTP_USER and SMTP_PASSWORD in .env file."
|
||||
error_msg = 'SMTP mailer not configured. Set SMTP_HOST, SMTP_USER and SMTP_PASSWORD in .env file.'
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Create message
|
||||
message = MIMEMultipart("alternative")
|
||||
message["From"] = from_header
|
||||
message["To"] = to
|
||||
message["Subject"] = subject
|
||||
|
||||
message = MIMEMultipart('alternative')
|
||||
message['From'] = from_header
|
||||
message['To'] = to
|
||||
message['Subject'] = subject
|
||||
if text:
|
||||
message.attach(MIMEText(text, "plain"))
|
||||
message.attach(MIMEText(text, 'plain'))
|
||||
if html:
|
||||
message.attach(MIMEText(html, "html"))
|
||||
|
||||
# If no content provided, add a default text
|
||||
if not text and not html:
|
||||
message.attach(MIMEText("", "plain"))
|
||||
|
||||
# Determine TLS/SSL settings
|
||||
# For port 465: use SSL/TLS (use_tls=True, start_tls=False)
|
||||
# For port 587: use STARTTLS (use_tls=False, start_tls=True)
|
||||
# For port 25: plain (usually not used for authenticated sending)
|
||||
message.attach(MIMEText(html, 'html'))
|
||||
if not text and (not html):
|
||||
message.attach(MIMEText('', 'plain'))
|
||||
if mail_port == 465 or mail_use_tls:
|
||||
# SSL/TLS connection (port 465)
|
||||
use_tls = True
|
||||
start_tls = False
|
||||
elif mail_port == 587:
|
||||
# STARTTLS connection (port 587)
|
||||
use_tls = False
|
||||
start_tls = True
|
||||
else:
|
||||
# Plain connection (port 25 or other)
|
||||
use_tls = False
|
||||
start_tls = False
|
||||
|
||||
logger.info(f"Attempting to send email to {to} via {mail_host}:{mail_port} (use_tls: {use_tls}, start_tls: {start_tls})")
|
||||
|
||||
# Send email using SMTP client
|
||||
smtp_client = aiosmtplib.SMTP(
|
||||
hostname=mail_host,
|
||||
port=mail_port,
|
||||
use_tls=use_tls,
|
||||
start_tls=start_tls,
|
||||
username=mail_user,
|
||||
password=mail_pass,
|
||||
)
|
||||
|
||||
logger.info(f'Attempting to send email to {to} via {mail_host}:{mail_port} (use_tls: {use_tls}, start_tls: {start_tls})')
|
||||
smtp_client = aiosmtplib.SMTP(hostname=mail_host, port=mail_port, use_tls=use_tls, start_tls=start_tls, username=mail_user, password=mail_pass)
|
||||
try:
|
||||
await smtp_client.connect()
|
||||
# Authentication happens automatically if username/password are provided in constructor
|
||||
await smtp_client.send_message(message)
|
||||
logger.info(f"Email sent successfully to {to}")
|
||||
logger.info(f'Email sent successfully to {to}')
|
||||
finally:
|
||||
await smtp_client.quit()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to send email to {to}: {type(e).__name__}: {str(e)}"
|
||||
error_msg = f'Failed to send email to {to}: {type(e).__name__}: {str(e)}'
|
||||
logger.error(error_msg, exc_info=True)
|
||||
raise
|
||||
|
||||
raise
|
||||
@@ -1,22 +1,11 @@
|
||||
"""
|
||||
VNPay integration removed
|
||||
This file is intentionally left as a stub to indicate the VNPay
|
||||
payment gateway has been removed from the project.
|
||||
"""
|
||||
|
||||
|
||||
def create_payment_url(*args, **kwargs):
|
||||
raise NotImplementedError("VNPay integration has been removed")
|
||||
|
||||
raise NotImplementedError('VNPay integration has been removed')
|
||||
|
||||
def verify_return(*args, **kwargs):
|
||||
raise NotImplementedError("VNPay integration has been removed")
|
||||
|
||||
raise NotImplementedError('VNPay integration has been removed')
|
||||
|
||||
def sort_object(obj):
|
||||
return {}
|
||||
|
||||
|
||||
def create_signature(*args, **kwargs):
|
||||
return ""
|
||||
|
||||
return ''
|
||||
Reference in New Issue
Block a user