feat: Implement booking email notifications and consent handling in booking flow

This commit is contained in:
2026-05-23 10:16:25 +02:00
parent 0a11d366a5
commit 2a96e8bbb4
5 changed files with 175 additions and 2 deletions
+130
View File
@@ -10,9 +10,11 @@ import shutil
import tarfile
import tempfile
import threading
import smtplib
from datetime import timedelta, datetime, date
from functools import wraps
from io import BytesIO
from email.message import EmailMessage
from urllib.parse import parse_qs, quote, urlparse
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
@@ -22,6 +24,7 @@ from pymongo import MongoClient
from pymongo.errors import PyMongoError
from bson.objectid import ObjectId
import user as user_store
from buchungen import buchen
app = Flask(__name__)
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s"
@@ -58,6 +61,24 @@ BOOKING_FLOW_MAP = {
},
}
BOOKING_PRICE_MAP = {
"starter_package": 320.0,
"advanced_package": 0.0,
"inventarsystem": 250.0,
"buecherei": 150.0,
"terminverwaltung": 0.0,
"emailversand": 0.0,
"schultraeger": 0.0,
"telefonsupport": 20.0,
}
SMTP_HOST = os.environ.get("SMTP_HOST", "").strip()
SMTP_USERNAME = os.environ.get("SMTP_USERNAME", "").strip()
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "1").strip().lower() not in {"0", "false", "no"}
SMTP_FROM_ADDRESS = os.environ.get("SMTP_FROM_ADDRESS", SMTP_USERNAME or "noreply@localhost").strip()
BOOKING_ADMIN_EMAIL_FALLBACK = os.environ.get("BOOKING_ADMIN_EMAIL", "").strip()
@app.after_request
def set_security_headers(response):
@@ -103,6 +124,12 @@ def _env_int(name: str, default: int) -> int:
return default
try:
SMTP_PORT = max(int(os.environ.get("SMTP_PORT", "587")), 1)
except ValueError:
SMTP_PORT = 587
MONGO_MAX_POOL_SIZE = max(_env_int("MONGO_MAX_POOL_SIZE", 12), 1)
MONGO_MIN_POOL_SIZE = max(_env_int("MONGO_MIN_POOL_SIZE", 0), 0)
MONGO_MAX_IDLE_MS = max(_env_int("MONGO_MAX_IDLE_MS", 60000), 1000)
@@ -210,6 +237,7 @@ def _normalize_user_doc(doc: dict | None) -> dict | None:
"username": doc.get("Username") or doc.get("username"),
"display_name": doc.get("name") or doc.get("display_name") or doc.get("Username"),
"is_admin": bool(doc.get("Admin") or doc.get("is_admin", False)),
"email": doc.get("email") or doc.get("Email") or "",
"created_at": doc.get("created_at"),
}
@@ -230,6 +258,39 @@ def _list_users_for_admin() -> list:
return users
def _list_admin_users() -> list:
return [user for user in _list_users_for_admin() if user.get("is_admin")]
def _select_booking_admin() -> dict | None:
admins = _list_admin_users()
if admins:
return admins[0]
return None
def _send_booking_email(recipient_email: str, subject: str, body: str) -> tuple[bool, str]:
if not SMTP_HOST or not recipient_email:
return False, "SMTP nicht konfiguriert"
message = EmailMessage()
message["Subject"] = subject
message["From"] = SMTP_FROM_ADDRESS
message["To"] = recipient_email
message.set_content(body)
try:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=12) as smtp:
if SMTP_USE_TLS:
smtp.starttls()
if SMTP_USERNAME:
smtp.login(SMTP_USERNAME, SMTP_PASSWORD)
smtp.send_message(message)
return True, "E-Mail wurde versendet"
except Exception as exc:
return False, _sanitize_text(str(exc), 240)
def _sanitize_text(text: str, max_length: int = 255) -> str:
"""Sanitize user text input: strip, limit length, and escape HTML."""
text = (text or "").strip()
@@ -1871,6 +1932,7 @@ def booking_payment():
"billing_zip": _sanitize_text(request.values.get("billing_zip") or "", 20),
"billing_city": _sanitize_text(request.values.get("billing_city") or "", 120),
"notes": _sanitize_text(request.values.get("notes") or "", 600),
"agreements_accepted": (request.values.get("agreements_accepted") or "").strip().lower() in {"1", "on", "true", "yes"},
}
if request.method == "GET" and not form_data["contact_person"]:
@@ -1904,10 +1966,19 @@ def booking_payment():
flash(f"Bitte folgende Pflichtfelder ausfüllen: {', '.join(missing_labels)}.", "error")
return render_template("schnell_buchung.html", **review_data)
if not form_data["agreements_accepted"]:
flash("Bitte Nutzungsbedingungen, Lizenzvereinbarung und Datenschutzerklärung bestätigen.", "error")
return render_template("schnell_buchung.html", **review_data)
if not _validate_email(form_data["contact_email"]):
flash("Bitte eine gültige E-Mail-Adresse angeben.", "error")
return render_template("schnell_buchung.html", **review_data)
assigned_admin = _select_booking_admin()
assigned_admin_username = assigned_admin.get("username") if assigned_admin else ""
assigned_admin_display_name = assigned_admin.get("display_name") if assigned_admin else ""
assigned_admin_email = (assigned_admin.get("email") if assigned_admin else "") or BOOKING_ADMIN_EMAIL_FALLBACK
message = (
f"{flow_config['message_prefix']} {selected_package} {flow_config['message_suffix']}\n"
f"Ansprechpartner: {form_data['contact_person']}\n"
@@ -1920,6 +1991,16 @@ def booking_payment():
client = None
try:
if booking_flow == "payment":
buchen(
name=selected_package,
Straße=form_data["billing_street"],
plz=form_data["billing_zip"],
ort=form_data["billing_city"],
ansprech=form_data["contact_person"],
email=form_data["contact_email"],
)
client, col = _get_collection("chat_messages")
col.insert_one(
{
@@ -1933,6 +2014,55 @@ def booking_payment():
"booking_data": form_data,
}
)
if booking_flow == "payment":
booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
invoice_amount = BOOKING_PRICE_MAP.get(package_raw, 0.0)
invoice_payload = {
"username": session.get("username"),
"invoice_number": booking_number,
"period": "Sofortbuchung",
"amount_eur": invoice_amount,
"status": "Zu prüfen",
"due_date": date.today().isoformat(),
"pdf_path": "",
"created_at": _utc_now_iso(),
"source": "booking_payment",
"booking_flow": booking_flow,
"booking_package": package_raw,
"booking_package_label": selected_package,
"booking_data": form_data,
"paperwork_status": "Offen",
"paperwork_required": True,
"assigned_admin_username": assigned_admin_username,
"assigned_admin_display_name": assigned_admin_display_name,
"assigned_admin_email": assigned_admin_email,
"consent_accepted": True,
}
invoice_client = None
try:
invoice_client, invoice_col = _get_collection("invoices")
invoice_col.insert_one(invoice_payload)
finally:
if invoice_client:
invoice_client.close()
if assigned_admin_email:
email_subject = f"Neue Zahlungsbuchung: {selected_package}"
email_body = (
f"Es wurde eine neue direkte Buchung eingereicht.\n\n"
f"Paket: {selected_package}\n"
f"Ansprechpartner: {form_data['contact_person']}\n"
f"Schule: {form_data['school_name']}\n"
f"E-Mail: {form_data['contact_email']}\n"
f"Telefon: {form_data['contact_phone'] or '-'}\n"
f"Rechnungsadresse: {form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}\n"
f"Status: Bitte die Unterlagen in der Rechnungsverwaltung prüfen.\n"
f"Buchungsnummer: {booking_number}\n"
)
mail_ok, mail_message = _send_booking_email(assigned_admin_email, email_subject, email_body)
if not mail_ok:
flash(f"Rechnung wurde angelegt, E-Mail an den Admin konnte nicht gesendet werden: {mail_message}", "info")
except PyMongoError:
flash("Buchungsanfrage konnte nicht gesendet werden.", "error")
return redirect(url_for("booking_payment", package=package_raw, flow=booking_flow))