Files
space-booking/backend/app/services/email_service.py
Claude Agent e21cf03a16 feat: add multi-tenant system with properties, organizations, and public booking
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>
2026-02-15 00:17:21 +00:00

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)