232 lines
8.2 KiB
Python
232 lines
8.2 KiB
Python
"""Pure functions for building email context dicts from registration data."""
|
|||
|
|
|
||
|
|
from datetime import date, datetime
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
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"
|
||
|
|
|
||
|
|
_I18N_DIR = Path(__file__).parent / "i18n"
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# i18n
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def load_strings(language: str) -> dict:
|
||
|
|
"""Load the label/string table for *language* (falls back to German)."""
|
||
|
|
locale_file = _I18N_DIR / f"{language}.yaml"
|
||
|
|
if not locale_file.exists():
|
||
|
|
locale_file = _I18N_DIR / "de.yaml"
|
||
|
|
with locale_file.open(encoding="utf-8") as fh:
|
||
|
|
return yaml.safe_load(fh)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 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")
|
||
|
|
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}.-"
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 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,
|
||
|
|
"has_qr": has_qr,
|
||
|
|
"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,
|
||
|
|
}
|