644c99ee30
Sends a confirmation email (best-effort, fire-and-forget SMTP via mailer.py) on booking creation, with a manage-booking link embedding the ticket-2 signed token. Adds /manage/<token>, a stateless cancel/reschedule page that reuses the existing slot-picker against booking_api's create/cancel/reschedule API, distinguishing an invalid/expired link from an already-cancelled one. Sender address uses the client's own domain when configured, falling back to a mivanchenko.de address otherwise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""Customer self-service manage-booking page (#18), reached via the
|
|
token-linked link sent in the confirmation email (booking_mail.py). The
|
|
signed token (booking_api.verify_manage_token) is the only source of
|
|
identity here -- customers have no account, so this route needs no login.
|
|
|
|
The actual cancel/reschedule mutations still go through booking_api.py's
|
|
JSON API (#16); this route only resolves the token, decides which of the
|
|
three states (invalid/expired, already used, active) to render, and lets the
|
|
page's own JS drive the API from there.
|
|
"""
|
|
from flask import Blueprint, render_template
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import booking_db as bdb
|
|
from booking_api import verify_manage_token
|
|
|
|
bp = Blueprint("manage_booking", __name__)
|
|
|
|
|
|
@bp.get("/manage/<token>")
|
|
def manage_page(token):
|
|
resolved = verify_manage_token(token)
|
|
if resolved is None:
|
|
# Distinct from "already used" per #18's acceptance criteria -- an
|
|
# expired/malformed token never resolved to a booking at all.
|
|
return render_template("manage.html", state="invalid"), 400
|
|
|
|
client_id, booking_id = resolved
|
|
booking = bdb.get_booking(client_id, booking_id)
|
|
client = bdb.get_client(client_id)
|
|
if booking is None or client is None:
|
|
return render_template("manage.html", state="invalid"), 404
|
|
|
|
if booking["status"] == "cancelled":
|
|
return render_template("manage.html", state="used")
|
|
|
|
tz = ZoneInfo(client.get("timezone") or "Europe/Berlin")
|
|
duration_minutes = int(
|
|
(booking["end_time"] - booking["start_time"]).total_seconds() // 60)
|
|
return render_template(
|
|
"manage.html",
|
|
state="active",
|
|
token=token,
|
|
client_id=client_id,
|
|
booking_id=booking_id,
|
|
resource_id=booking["resource_id"],
|
|
service=booking["service"],
|
|
duration_minutes=duration_minutes,
|
|
start_local=booking["start_time"].astimezone(tz),
|
|
status=booking["status"],
|
|
)
|