Initial commit - Customer Portal for Coolify
This commit is contained in:
176
customer_portal/web/routes/bookings.py
Executable file
176
customer_portal/web/routes/bookings.py
Executable file
@@ -0,0 +1,176 @@
|
||||
"""Bookings routes.
|
||||
|
||||
Sprint 14: Added local database storage for bookings.
|
||||
Bookings can be synced from WordPress and stored locally.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
flash,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from customer_portal.models import get_db
|
||||
from customer_portal.models.booking import Booking
|
||||
from customer_portal.services.token import TokenService
|
||||
from customer_portal.services.wordpress_api import WordPressAPI
|
||||
from customer_portal.web.routes.auth import login_required
|
||||
|
||||
bp = Blueprint("bookings", __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@login_required
|
||||
def list_bookings():
|
||||
"""List customer bookings from local database.
|
||||
|
||||
Sprint 14: Now uses local database instead of WordPress API.
|
||||
Bookings are synced via admin panel.
|
||||
"""
|
||||
db = get_db()
|
||||
|
||||
# Get filter
|
||||
status_filter = request.args.get("status", "")
|
||||
|
||||
# Get bookings from local database
|
||||
query = db.query(Booking).filter(Booking.customer_id == g.customer.id)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Booking.status == status_filter)
|
||||
|
||||
bookings = query.order_by(Booking.kurs_date.desc()).all()
|
||||
|
||||
return render_template(
|
||||
"bookings/list.html",
|
||||
bookings=bookings,
|
||||
status_filter=status_filter,
|
||||
use_local_db=True,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<int:booking_id>")
|
||||
@login_required
|
||||
def detail(booking_id: int):
|
||||
"""Booking detail page from local database.
|
||||
|
||||
Sprint 14: Now uses local database.
|
||||
"""
|
||||
db = get_db()
|
||||
|
||||
booking = (
|
||||
db.query(Booking)
|
||||
.filter(
|
||||
Booking.id == booking_id,
|
||||
Booking.customer_id == g.customer.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not booking:
|
||||
flash("Buchung nicht gefunden oder kein Zugriff.", "error")
|
||||
return redirect(url_for("bookings.list_bookings"))
|
||||
|
||||
return render_template("bookings/detail.html", booking=booking, use_local_db=True)
|
||||
|
||||
|
||||
@bp.route("/<int:booking_id>/cancel", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def cancel(booking_id: int):
|
||||
"""Request booking cancellation.
|
||||
|
||||
Sprint 14: Uses local database for booking lookup,
|
||||
but still calls WordPress API for the actual cancellation.
|
||||
"""
|
||||
db = get_db()
|
||||
|
||||
# Get booking from local database
|
||||
booking = (
|
||||
db.query(Booking)
|
||||
.filter(
|
||||
Booking.id == booking_id,
|
||||
Booking.customer_id == g.customer.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not booking:
|
||||
flash("Buchung nicht gefunden oder kein Zugriff.", "error")
|
||||
return redirect(url_for("bookings.list_bookings"))
|
||||
|
||||
# Check if already cancelled or pending
|
||||
if booking.status in ("cancelled", "cancel_requested"):
|
||||
flash("Diese Buchung wurde bereits storniert oder ist in Bearbeitung.", "info")
|
||||
return redirect(url_for("bookings.detail", booking_id=booking_id))
|
||||
|
||||
if request.method == "POST":
|
||||
reason = request.form.get("reason", "").strip()
|
||||
|
||||
try:
|
||||
# Call WordPress API to cancel (uses wp_booking_id)
|
||||
result = WordPressAPI.cancel_booking(
|
||||
booking.wp_booking_id, g.customer.email, reason
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
# Update local status
|
||||
booking.status = "cancel_requested"
|
||||
db.commit()
|
||||
|
||||
flash(
|
||||
"Stornierungsanfrage wurde gesendet. Sie erhalten eine Bestätigung per E-Mail.",
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for("bookings.detail", booking_id=booking_id))
|
||||
else:
|
||||
error = result.get("error", "Unbekannter Fehler")
|
||||
flash(f"Stornierung fehlgeschlagen: {error}", "error")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling booking {booking_id}: {e}")
|
||||
flash("Stornierung konnte nicht durchgeführt werden.", "error")
|
||||
|
||||
return render_template("bookings/cancel.html", booking=booking, use_local_db=True)
|
||||
|
||||
|
||||
@bp.route("/book/<int:kurs_id>")
|
||||
@login_required
|
||||
def book_kurs(kurs_id: int):
|
||||
"""Redirect to WordPress booking with pre-filled customer data.
|
||||
|
||||
Generates a signed token containing customer data and redirects
|
||||
to the WordPress kurs page with the token as URL parameter.
|
||||
WordPress validates the token and pre-fills the booking form.
|
||||
|
||||
Args:
|
||||
kurs_id: WordPress post ID of the kurs to book
|
||||
"""
|
||||
try:
|
||||
token = TokenService.generate_prefill_token(g.customer)
|
||||
except ValueError as e:
|
||||
logger.error(f"Token generation failed: {e}")
|
||||
flash("Verbindung zu WordPress nicht konfiguriert.", "error")
|
||||
return redirect(url_for("bookings.list_bookings"))
|
||||
|
||||
# Get WordPress base URL (remove API path).
|
||||
wp_api_url = current_app.config.get("WP_API_URL", "")
|
||||
wp_base_url = wp_api_url.replace("/wp-json/kurs-booking/v1", "")
|
||||
|
||||
if not wp_base_url:
|
||||
logger.error("WP_API_URL not configured")
|
||||
flash("WordPress-URL nicht konfiguriert.", "error")
|
||||
return redirect(url_for("bookings.list_bookings"))
|
||||
|
||||
# Build URL to kurs page with prefill token.
|
||||
# Using ?p=ID format for compatibility with any permalink structure.
|
||||
booking_url = f"{wp_base_url}/?p={kurs_id}&kb_prefill={token}"
|
||||
|
||||
logger.info(f"Redirecting customer {g.customer.id} to book kurs {kurs_id}")
|
||||
return redirect(booking_url)
|
||||
Reference in New Issue
Block a user