2026-02-23 10:05:03 +00:00
|
|
|
"""Pure functions for building email context dicts from registration data."""
|
|
|
|
|
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
|
|
|
|
|
from ..models.registration import RegistrationData
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Swiss QR-bill payment constants (stable bank details — not in config)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
QR_IBAN = "CH14 0900 0000 4930 8018 8"
|
|
|
|
|
QR_PAYEE = "Familienverein Fällanden Spielgruppen"
|
|
|
|
|
QR_STREET = "Huebwisstrase 5"
|
|
|
|
|
QR_PCODE = "8117"
|
|
|
|
|
QR_CITY = "Fällanden"
|
|
|
|
|
|
2026-02-27 19:18:59 +00:00
|
|
|
LEADER_INDOOR_NAME = "Andrea Sigrist"
|
|
|
|
|
LEADER_INDOOR_PHONE = "079 674 99 92"
|
|
|
|
|
LEADER_INDOOR_EMAIL = "andrea.sigrist@gmx.net"
|
|
|
|
|
|
|
|
|
|
LEADER_OUTDOOR_NAME = "Barbara Gross"
|
|
|
|
|
LEADER_OUTDOOR_PHONE = "078 761 19 64"
|
|
|
|
|
LEADER_OUTDOOR_EMAIL = "baba.laeubli@gmail.com"
|
|
|
|
|
|
|
|
|
|
ADMIN_NAME = "Markus Graf"
|
|
|
|
|
ADMIN_PHONE = "079 261 16 37"
|
|
|
|
|
ADMIN_EMAIL = "spielgruppen@familien-verein.ch"
|
|
|
|
|
|
2026-02-23 10:05:03 +00:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Formatting helpers (pure functions, no side-effects)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_dob(dob_str: str) -> str:
|
|
|
|
|
"""Return DD.MM.YYYY from a YYYY-MM-DD string, or the original on error."""
|
|
|
|
|
try:
|
|
|
|
|
return datetime.strptime(dob_str, "%Y-%m-%d").strftime("%d.%m.%Y")
|
|
|
|
|
except Exception:
|
|
|
|
|
return dob_str or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_age(dob_str: str) -> str:
|
|
|
|
|
"""Return 'X Jahre, Y Monate' from a YYYY-MM-DD string."""
|
|
|
|
|
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} Jahre, {months} Monate"
|
|
|
|
|
except Exception:
|
|
|
|
|
return dob_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_types(types: list[str]) -> str:
|
|
|
|
|
"""German label for a list of playgroup type keys (admin emails)."""
|
|
|
|
|
has_indoor = "indoor" in types
|
|
|
|
|
has_outdoor = "outdoor" in types
|
|
|
|
|
if has_indoor and has_outdoor:
|
|
|
|
|
return "Innen- und Waldspielgruppe"
|
|
|
|
|
if has_indoor:
|
|
|
|
|
return "Innenspielgruppe"
|
|
|
|
|
if has_outdoor:
|
|
|
|
|
return "Waldspielgruppe"
|
|
|
|
|
return "Spielgruppe"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_types_i18n(types: list[str], strings: dict) -> str:
|
|
|
|
|
"""Localised label for playgroup type keys using the supplied string table."""
|
|
|
|
|
type_map: dict = strings["types"]
|
|
|
|
|
labels = [type_map.get(t, t) for t in types]
|
|
|
|
|
return ", ".join(labels) if labels else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_days(registration: RegistrationData) -> str:
|
|
|
|
|
"""German day + type labels for admin emails."""
|
|
|
|
|
day_map = {"monday": "Montag", "wednesday": "Mittwoch", "thursday": "Donnerstag"}
|
|
|
|
|
type_map = {"indoor": "Innenspielgruppe", "outdoor": "Waldspielgruppe"}
|
|
|
|
|
return ", ".join(
|
|
|
|
|
f"{day_map.get(d.day, d.day.capitalize())} ({type_map.get(d.type, d.type)})"
|
|
|
|
|
for d in registration.booking.selected_days
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_days_i18n(registration: RegistrationData, strings: dict) -> str:
|
|
|
|
|
"""Localised day + type labels using the supplied string table."""
|
|
|
|
|
day_map: dict = strings["days"]
|
|
|
|
|
type_map: dict = strings["types"]
|
|
|
|
|
return ", ".join(
|
|
|
|
|
f"{day_map.get(d.day, d.day.capitalize())} ({type_map.get(d.type, d.type)})"
|
|
|
|
|
for d in registration.booking.selected_days
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_monthly_fee(registration: RegistrationData) -> str:
|
|
|
|
|
"""Compute the monthly fee string from the booking selection."""
|
|
|
|
|
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")
|
2026-02-23 15:06:47 +00:00
|
|
|
fee = indoor_days * 130 + outdoor_days * 250
|
2026-02-23 10:05:03 +00:00
|
|
|
return f"CHF {fee}.-"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Context builders
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_admin_new_context(
|
|
|
|
|
registration: RegistrationData,
|
|
|
|
|
registration_id: str,
|
|
|
|
|
version: int,
|
|
|
|
|
channel: str,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Build the template context for the admin new-registration email."""
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
pg = registration.parent_guardian
|
|
|
|
|
ec = registration.emergency_contact
|
|
|
|
|
ch = registration.child
|
|
|
|
|
channel_de = {"email": "E-Mail", "chat": "Chat"}.get(channel.lower(), channel.title())
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"submitted_date": now.strftime("%d.%m.%Y"),
|
|
|
|
|
"submitted_time": now.strftime("%H:%M"),
|
|
|
|
|
"channel": channel_de,
|
|
|
|
|
"registration_id": registration_id,
|
|
|
|
|
"version": version,
|
|
|
|
|
"child_name": ch.full_name or "",
|
|
|
|
|
"child_dob": format_dob(ch.date_of_birth or ""),
|
|
|
|
|
"child_age": calculate_age(ch.date_of_birth or ""),
|
|
|
|
|
"child_needs": ch.special_needs or "Keine",
|
|
|
|
|
"playgroup_types": format_types(registration.booking.playgroup_types),
|
|
|
|
|
"days": format_days(registration),
|
|
|
|
|
"monthly_fee": calculate_monthly_fee(registration),
|
|
|
|
|
"parent_name": pg.full_name or "",
|
|
|
|
|
"parent_street": pg.street_address or "",
|
|
|
|
|
"parent_postal_code": pg.postal_code or "",
|
|
|
|
|
"parent_city": pg.city or "",
|
|
|
|
|
"parent_phone": pg.phone or "",
|
|
|
|
|
"parent_email": pg.email or "",
|
|
|
|
|
"emergency_name": ec.full_name or "",
|
|
|
|
|
"emergency_phone": ec.phone or "",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_admin_update_context(
|
|
|
|
|
registration: RegistrationData,
|
|
|
|
|
registration_id: str,
|
|
|
|
|
version: int,
|
|
|
|
|
change_summary: dict,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Build the template context for the admin registration-update email."""
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
pg = registration.parent_guardian
|
|
|
|
|
|
|
|
|
|
changes = [
|
|
|
|
|
{"field": field_path, "old": values["old"], "new": values["new"]}
|
|
|
|
|
for field_path, values in sorted(change_summary.items())
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"updated_date": now.strftime("%d.%m.%Y"),
|
|
|
|
|
"updated_time": now.strftime("%H:%M"),
|
|
|
|
|
"registration_id": registration_id,
|
|
|
|
|
"version": version,
|
|
|
|
|
"child_name": registration.child.full_name or "",
|
|
|
|
|
"parent_email": pg.email or "",
|
|
|
|
|
"changes": changes,
|
|
|
|
|
"playgroup_types": format_types(registration.booking.playgroup_types),
|
|
|
|
|
"days": format_days(registration),
|
|
|
|
|
"monthly_fee": calculate_monthly_fee(registration),
|
|
|
|
|
"parent_name": pg.full_name or "",
|
|
|
|
|
"parent_street": pg.street_address or "",
|
|
|
|
|
"parent_postal_code": pg.postal_code or "",
|
|
|
|
|
"parent_city": pg.city or "",
|
|
|
|
|
"parent_phone": pg.phone or "",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_parent_context(
|
|
|
|
|
registration: RegistrationData,
|
|
|
|
|
strings: dict,
|
|
|
|
|
has_qr: bool = True,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Build the template context for the parent confirmation email."""
|
|
|
|
|
pg = registration.parent_guardian
|
|
|
|
|
ec = registration.emergency_contact
|
|
|
|
|
ch = registration.child
|
|
|
|
|
parent_name = pg.full_name or pg.email or ""
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"lang": "de" if strings.get("none") == "Keine" else "en",
|
|
|
|
|
"strings": strings,
|
|
|
|
|
"greeting": strings["greeting"].format(name=parent_name),
|
|
|
|
|
"child_name": ch.full_name or "",
|
|
|
|
|
"child_dob": format_dob(ch.date_of_birth or ""),
|
|
|
|
|
"child_needs": ch.special_needs or strings["none"],
|
|
|
|
|
"playgroup_types": format_types_i18n(registration.booking.playgroup_types, strings),
|
|
|
|
|
"days": format_days_i18n(registration, strings),
|
|
|
|
|
"monthly_fee": calculate_monthly_fee(registration),
|
|
|
|
|
"has_indoor": "indoor" in registration.booking.playgroup_types,
|
2026-02-27 19:18:59 +00:00
|
|
|
"has_outdoor": "outdoor" in registration.booking.playgroup_types,
|
2026-02-23 10:05:03 +00:00
|
|
|
"has_qr": has_qr,
|
2026-02-27 19:18:59 +00:00
|
|
|
"leader_indoor_name": LEADER_INDOOR_NAME,
|
|
|
|
|
"leader_indoor_phone": LEADER_INDOOR_PHONE,
|
|
|
|
|
"leader_indoor_email": LEADER_INDOOR_EMAIL,
|
|
|
|
|
"leader_outdoor_name": LEADER_OUTDOOR_NAME,
|
|
|
|
|
"leader_outdoor_phone": LEADER_OUTDOOR_PHONE,
|
|
|
|
|
"leader_outdoor_email": LEADER_OUTDOOR_EMAIL,
|
|
|
|
|
"admin_name": ADMIN_NAME,
|
|
|
|
|
"admin_phone": ADMIN_PHONE,
|
|
|
|
|
"admin_email": ADMIN_EMAIL,
|
2026-02-23 10:05:03 +00:00
|
|
|
"parent_name": pg.full_name or "",
|
|
|
|
|
"parent_address": pg.street_address or "",
|
|
|
|
|
"parent_postal_code": pg.postal_code or "",
|
|
|
|
|
"parent_city": pg.city or "",
|
|
|
|
|
"parent_phone": pg.phone or "",
|
|
|
|
|
"parent_email": pg.email or "",
|
|
|
|
|
"emergency_name": ec.full_name or "",
|
|
|
|
|
"emergency_phone": ec.phone or "",
|
|
|
|
|
"iban": QR_IBAN,
|
|
|
|
|
"payee": QR_PAYEE,
|
|
|
|
|
"payee_street": QR_STREET,
|
|
|
|
|
"payee_postal_code": QR_PCODE,
|
|
|
|
|
"payee_city": QR_CITY,
|
|
|
|
|
}
|