Files
space-booking/backend/app/services/email_service.py
Claude Agent a15951c645 feat: sistem de linkuri publice pentru proprietăți
Expune fluxul de rezervare anonimă printr-un link public partajabil
(/book/<slug>), cu calendar public, cod QR și proprietate demo publică.

Backend:
- slug + list_on_landing pe Property; utilitar slugify (diacritice RO,
  cuvinte rezervate, ban all-digit) + migrare idempotentă raw-DDL rulată
  din entrypoint; SQLite WAL + busy_timeout
- GET /public/properties/{slug_or_id}(+/spaces): rezolvare slug/ID, 404 uniform
- GET /public/spaces/{id}/busy: doar {start_time,end_time} (fără PII),
  pending+approved, tz-aware/interval>60z → 422, orfan/privat → 404
- availability: scrub user_name/title (fix scurgere PII)
- POST /public/bookings: overlap pending+approved, cap 5/email/spațiu/zi,
  rate-limit per-IP, suprimare notificări+email pe demo, email confirmare guest
- PATCH slug (409 duplicat / 422 format / 403 non-manager); guest_email EmailStr
- email guest la aprobare/respingere cu cod #RB-<id>
- teste noi: test_public.py + test_migrate_slug.py (33 teste)

Frontend:
- rută /book/:slug; PublicBooking rescris cu calendar grid manual
  (PublicCalendar + GuestBookingForm), 30min, 08-20, mobil day-view, RO
- card „Link public" în PropertyDetail: copy, edit slug, QR pe URL-ul cu ID
- secțiune proprietăți publice + CTA pe Landing; buton copy în Properties

Gate aprobat (U1-U3, G1 grid manual, G2 list_on_landing); G3-G5 în TODOS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:04:58 +00:00

210 lines
5.7 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":
reference_code = extra_data.get("reference_code", f"RB-{booking.id}") if extra_data else f"RB-{booking.id}"
subject = "Rezervare Aprobată"
body = f"""Bună ziua {user_name},
Rezervarea dumneavoastră a fost aprobată:
Cod de referință: #{reference_code}
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"
reference_code = extra_data.get("reference_code", f"RB-{booking.id}") if extra_data else f"RB-{booking.id}"
subject = "Rezervare Respinsă"
body = f"""Bună ziua {user_name},
Rezervarea dumneavoastră a fost respinsă:
Cod de referință: #{reference_code}
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)