Implement complete multi-property architecture: - Properties (groups of spaces) with public/private visibility - Property managers (many-to-many) with role-based permissions - Organizations with member management - Anonymous/guest booking support via public API (/api/public/*) - Property-scoped spaces, bookings, and settings - Frontend: property selector, organization management, public booking views - Migration script and updated seed data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
5.4 KiB
Python
206 lines
5.4 KiB
Python
"""Email service for sending booking notifications."""
|
|
import logging
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from typing import Optional
|
|
|
|
import aiosmtplib
|
|
|
|
from app.core.config import settings
|
|
from app.models.booking import Booking
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def send_email(to: str, subject: str, body: str) -> bool:
|
|
"""Send email via SMTP. Returns True if successful."""
|
|
if not settings.smtp_enabled:
|
|
# Development mode: just log the email
|
|
logger.info(f"[EMAIL] To: {to}")
|
|
logger.info(f"[EMAIL] Subject: {subject}")
|
|
logger.info(f"[EMAIL] Body:\n{body}")
|
|
print(f"\n--- EMAIL ---")
|
|
print(f"To: {to}")
|
|
print(f"Subject: {subject}")
|
|
print(f"Body:\n{body}")
|
|
print(f"--- END EMAIL ---\n")
|
|
return True
|
|
|
|
try:
|
|
message = MIMEMultipart()
|
|
message["From"] = settings.smtp_from_address
|
|
message["To"] = to
|
|
message["Subject"] = subject
|
|
message.attach(MIMEText(body, "plain", "utf-8"))
|
|
|
|
await aiosmtplib.send(
|
|
message,
|
|
hostname=settings.smtp_host,
|
|
port=settings.smtp_port,
|
|
username=settings.smtp_user if settings.smtp_user else None,
|
|
password=settings.smtp_password if settings.smtp_password else None,
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email to {to}: {e}")
|
|
return False
|
|
|
|
|
|
def generate_booking_email(
|
|
booking: Booking,
|
|
event_type: str,
|
|
user_email: str,
|
|
user_name: str,
|
|
extra_data: Optional[dict] = None,
|
|
) -> tuple[str, str]:
|
|
"""Generate email subject and body for booking events.
|
|
|
|
Returns: (subject, body)
|
|
"""
|
|
extra_data = extra_data or {}
|
|
space_name = booking.space.name if booking.space else "Unknown Space"
|
|
start_str = booking.start_datetime.strftime("%d.%m.%Y %H:%M")
|
|
end_str = booking.end_datetime.strftime("%H:%M")
|
|
|
|
if event_type == "created":
|
|
subject = "Cerere Nouă de Rezervare"
|
|
body = f"""Bună ziua,
|
|
|
|
O nouă cerere de rezervare necesită aprobarea dumneavoastră:
|
|
|
|
Utilizator: {user_name}
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
Descriere: {booking.description or 'N/A'}
|
|
|
|
Vă rugăm să accesați panoul de administrare pentru a aproba sau respinge această cerere.
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "approved":
|
|
subject = "Rezervare Aprobată"
|
|
body = f"""Bună ziua {user_name},
|
|
|
|
Rezervarea dumneavoastră a fost aprobată:
|
|
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
|
|
Vă așteptăm!
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "rejected":
|
|
reason = extra_data.get("rejection_reason", "Nu a fost specificat")
|
|
subject = "Rezervare Respinsă"
|
|
body = f"""Bună ziua {user_name},
|
|
|
|
Rezervarea dumneavoastră a fost respinsă:
|
|
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
Motiv: {reason}
|
|
|
|
Vă rugăm să contactați administratorul pentru detalii.
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "canceled":
|
|
reason = extra_data.get("cancellation_reason", "Nu a fost specificat")
|
|
subject = "Rezervare Anulată"
|
|
body = f"""Bună ziua {user_name},
|
|
|
|
Rezervarea dumneavoastră a fost anulată de către administrator:
|
|
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
Motiv: {reason}
|
|
|
|
Vă rugăm să contactați administratorul pentru detalii.
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "anonymous_created":
|
|
guest_email = extra_data.get("guest_email", "N/A") if extra_data else "N/A"
|
|
subject = "Cerere Anonimă de Rezervare"
|
|
body = f"""Bună ziua,
|
|
|
|
O nouă cerere anonimă de rezervare necesită aprobarea dumneavoastră:
|
|
|
|
Persoana: {user_name}
|
|
Email: {guest_email}
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
Descriere: {booking.description or 'N/A'}
|
|
|
|
Vă rugăm să accesați panoul de administrare pentru a aproba sau respinge această cerere.
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "anonymous_approved":
|
|
subject = "Rezervare Aprobată"
|
|
body = f"""Bună ziua {user_name},
|
|
|
|
Rezervarea dumneavoastră a fost aprobată:
|
|
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
|
|
Vă așteptăm!
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
elif event_type == "anonymous_rejected":
|
|
reason = extra_data.get("rejection_reason", "Nu a fost specificat") if extra_data else "Nu a fost specificat"
|
|
subject = "Rezervare Respinsă"
|
|
body = f"""Bună ziua {user_name},
|
|
|
|
Rezervarea dumneavoastră a fost respinsă:
|
|
|
|
Spațiu: {space_name}
|
|
Data și ora: {start_str} - {end_str}
|
|
Titlu: {booking.title}
|
|
Motiv: {reason}
|
|
|
|
Cu stimă,
|
|
Sistemul de Rezervări
|
|
"""
|
|
|
|
else:
|
|
subject = "Notificare Rezervare"
|
|
body = f"Notificare despre rezervarea pentru {space_name} din {start_str}"
|
|
|
|
return subject, body
|
|
|
|
|
|
async def send_booking_notification(
|
|
booking: Booking,
|
|
event_type: str,
|
|
user_email: str,
|
|
user_name: str,
|
|
extra_data: Optional[dict] = None,
|
|
) -> bool:
|
|
"""Send booking notification email."""
|
|
subject, body = generate_booking_email(
|
|
booking, event_type, user_email, user_name, extra_data
|
|
)
|
|
return await send_email(user_email, subject, body)
|