Booking confirmation email + customer self-service cancel/reschedule (#18)
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>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""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"],
|
||||
)
|
||||
Reference in New Issue
Block a user