Files
Meister-Eder/src/notifications/notifier.py
T

242 lines
8.4 KiB
Python
Raw Normal View History

"""Admin email notifications sent when a registration is completed."""
import logging
import smtplib
from datetime import date, datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from ..models.registration import RegistrationData
logger = logging.getLogger(__name__)
# Notification routing per spec
_INDOOR_EMAIL = "andrea.sigrist@gmx.net"
_OUTDOOR_EMAIL = "baba.laeubli@gmail.com"
_ADMIN_CC_EMAIL = "spielgruppen@familien-verein.ch"
class AdminNotifier:
"""Sends formatted admin notification emails upon registration completion.
The SMTP credentials are re-used from the agent's outbound email config.
When *smtp_host* is empty the notifier logs the notification and skips
sending (useful for local development / testing).
"""
def __init__(
self,
smtp_host: str,
smtp_port: int,
username: str,
password: str,
use_tls: bool = True,
from_email: str = "",
) -> None:
self._smtp_host = smtp_host
self._smtp_port = smtp_port
self._username = username
self._password = password
self._use_tls = use_tls
self._from_email = from_email or username
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def notify_admin(
self,
registration: RegistrationData,
registration_id: str,
conversation_id: str,
channel: str,
) -> None:
"""Send notification email(s) for a completed registration."""
types = registration.booking.playgroup_types
to_addresses: list[str] = []
if "indoor" in types:
to_addresses.append(_INDOOR_EMAIL)
if "outdoor" in types:
to_addresses.append(_OUTDOOR_EMAIL)
subject = (
f"New Registration: {registration.child.full_name} "
f"for {self._format_types(types)}"
)
body = self._build_body(registration, registration_id, channel)
self._send(
to=to_addresses,
cc=[_ADMIN_CC_EMAIL],
subject=subject,
body=body,
reply_to=registration.parent_guardian.email or "",
)
# ------------------------------------------------------------------
# Formatting helpers
# ------------------------------------------------------------------
@staticmethod
def _format_types(types: list[str]) -> str:
has_indoor = "indoor" in types
has_outdoor = "outdoor" in types
if has_indoor and has_outdoor:
return "Indoor + Outdoor Playgroup"
if has_indoor:
return "Indoor Playgroup"
if has_outdoor:
return "Outdoor Playgroup"
return "Playgroup"
@staticmethod
def _calculate_age(dob_str: str) -> str:
try:
dob = datetime.strptime(dob_str, "%Y-%m-%d").date()
today = date.today()
years = today.year - dob.year - (
(today.month, today.day) < (dob.month, dob.day)
)
months = (today.month - dob.month) % 12
return f"{years} years, {months} months"
except Exception:
return dob_str
@staticmethod
def _format_dob(dob_str: str) -> str:
try:
return datetime.strptime(dob_str, "%Y-%m-%d").strftime("%d.%m.%Y")
except Exception:
return dob_str or ""
@staticmethod
def _calculate_monthly_fee(registration: RegistrationData) -> str:
indoor_days = sum(1 for d in registration.booking.selected_days if d.type == "indoor")
outdoor_days = sum(1 for d in registration.booking.selected_days if d.type == "outdoor")
fee = 0
if indoor_days == 1:
fee += 130
elif indoor_days == 2:
fee += 260
elif indoor_days >= 3:
fee += 390
if outdoor_days >= 1:
fee += 250
return f"CHF {fee}.-"
@staticmethod
def _format_days(registration: RegistrationData) -> str:
day_map = {
"monday": "Monday",
"wednesday": "Wednesday",
"thursday": "Thursday",
}
return ", ".join(
f"{day_map.get(d.day, d.day.capitalize())} ({d.type})"
for d in registration.booking.selected_days
)
def _build_body(
self,
registration: RegistrationData,
registration_id: str,
channel: str,
) -> str:
now = datetime.utcnow()
pg = registration.parent_guardian
ec = registration.emergency_contact
return (
"===============================================\n"
"NEW PLAYGROUP REGISTRATION\n"
"===============================================\n"
"\n"
f"Submitted: {now.strftime('%d.%m.%Y')} at {now.strftime('%H:%M')} UTC\n"
f"Channel: {channel.title()}\n"
f"Registration ID: {registration_id}\n"
"\n"
"-----------------------------------------------\n"
"CHILD INFORMATION\n"
"-----------------------------------------------\n"
f"Name: {registration.child.full_name}\n"
f"Date of Birth: {self._format_dob(registration.child.date_of_birth or '')} "
f"(Age: {self._calculate_age(registration.child.date_of_birth or '')})\n"
f"Special Needs: {registration.child.special_needs or 'None'}\n"
"\n"
"-----------------------------------------------\n"
"PLAYGROUP SELECTION\n"
"-----------------------------------------------\n"
f"Type: {self._format_types(registration.booking.playgroup_types)}\n"
f"Days: {self._format_days(registration)}\n"
"\n"
f"Monthly Fee: {self._calculate_monthly_fee(registration)}\n"
"(Plus CHF 80 registration fee if first enrolment)\n"
"\n"
"-----------------------------------------------\n"
"PARENT / GUARDIAN\n"
"-----------------------------------------------\n"
f"Name: {pg.full_name}\n"
f"Address: {pg.street_address}\n"
f" {pg.postal_code} {pg.city}\n"
f"Phone: {pg.phone}\n"
f"Email: {pg.email}\n"
"\n"
"-----------------------------------------------\n"
"EMERGENCY CONTACT\n"
"-----------------------------------------------\n"
f"Name: {ec.full_name}\n"
f"Phone: {ec.phone}\n"
"\n"
"===============================================\n"
"\n"
"This registration was submitted via the automated registration assistant.\n"
)
# ------------------------------------------------------------------
# SMTP dispatch
# ------------------------------------------------------------------
def _send(
self,
to: list[str],
cc: list[str],
subject: str,
body: str,
reply_to: str = "",
) -> None:
if not self._smtp_host:
logger.warning(
"SMTP not configured — notification NOT sent. Would have emailed %s (CC: %s): %s",
to,
cc,
subject,
)
logger.debug("Notification body:\n%s", body)
return
msg = MIMEMultipart("alternative")
msg["From"] = self._from_email
msg["To"] = ", ".join(to)
msg["CC"] = ", ".join(cc)
msg["Subject"] = subject
if reply_to:
msg["Reply-To"] = reply_to
msg.attach(MIMEText(body, "plain", "utf-8"))
all_recipients = to + cc
try:
if self._use_tls:
server = smtplib.SMTP(self._smtp_host, self._smtp_port)
server.starttls()
else:
server = smtplib.SMTP_SSL(self._smtp_host, self._smtp_port)
server.login(self._username, self._password)
server.sendmail(self._from_email, all_recipients, msg.as_string())
server.quit()
logger.info("Admin notification sent to %s", all_recipients)
except Exception:
logger.exception("Failed to send admin notification to %s", all_recipients)