""" app/tenant/booking/routes.py Public online customer booking — /book/ No authentication required. """ import logging from datetime import datetime, timezone, timedelta, date from flask import Blueprint, render_template, request, g from app.extensions import db, limiter, csrf, mail from app.models.platform import Tenant from app.models.salon import Appointment, Customer, Staff, Service, Location from app.decorators import tenant_feature_required logger = logging.getLogger(__name__) booking_bp = Blueprint("booking", __name__) @booking_bp.route("/book/", methods=["GET", "POST"]) @limiter.limit("15 per minute") @csrf.exempt def public_booking(tenant_slug): tenant = Tenant.query.filter_by(slug=tenant_slug).filter( Tenant.status.in_(["active", "trial"]) ).first() if not tenant: return render_template("tenant/booking/not_found.html"), 404 if not tenant.has_feature("online_booking"): return render_template("tenant/booking/unavailable.html", tenant=tenant), 403 location = Location.query.filter_by( tenant_id=tenant.id, is_primary=True, is_active=True ).first() or Location.query.filter_by( tenant_id=tenant.id, is_active=True).first() if not location: return render_template("tenant/booking/unavailable.html", tenant=tenant), 403 services = Service.query.filter_by( tenant_id=tenant.id, is_active=True).filter( Service.deleted_at.is_(None)).order_by(Service.name).all() staff_list = Staff.query.filter_by( tenant_id=tenant.id, is_active=True).filter( Staff.deleted_at.is_(None)).order_by(Staff.name).all() confirmed = False error = None if request.method == "POST": name = request.form.get("name", "").strip()[:255] phone = request.form.get("phone", "").strip()[:30] email = request.form.get("email", "").strip()[:255] or None service_id = request.form.get("service_id", type=int) staff_id = request.form.get("staff_id", type=int) or None date_raw = request.form.get("preferred_date", "") time_raw = request.form.get("preferred_time", "") notes = request.form.get("notes", "").strip()[:500] or None if not name or not phone or not service_id or not date_raw or not time_raw: error = "Please complete all required fields." else: try: start_time = datetime.fromisoformat(f"{date_raw}T{time_raw}") start_time = start_time.replace(tzinfo=timezone.utc) except ValueError: error = "Invalid date or time." start_time = None if start_time and start_time < datetime.now(timezone.utc): error = "Please choose a future date and time." if not error: svc = Service.query.filter_by( id=service_id, tenant_id=tenant.id).first() duration = svc.duration_min if svc else 30 end_time = start_time + timedelta(minutes=duration) # Look up or create customer customer = Customer.query.filter_by( tenant_id=tenant.id, phone=phone).filter( Customer.deleted_at.is_(None)).first() if not customer: customer = Customer( tenant_id=tenant.id, name=name, phone=phone, email=email, is_active=True, loyalty_points=0, no_show_count=0, ) db.session.add(customer) db.session.flush() appt = Appointment( tenant_id=tenant.id, location_id=location.id, customer_id=customer.id, staff_id=staff_id, service_id=service_id, start_time=start_time, end_time=end_time, is_walk_in=False, status="pending", notes=notes, rebook_source="online", ) db.session.add(appt) db.session.commit() logger.info("Online booking: tenant=%s appt=%s customer=%s", tenant.id, appt.id, customer.id) # Send confirmation email if email: try: from flask_mail import Message msg = Message( subject=f"Booking Confirmed — {tenant.name}", recipients=[email], body=( f"Hi {name},\n\nYour appointment has been requested.\n" f"Service: {svc.name if svc else 'N/A'}\n" f"Date: {start_time.strftime('%B %d, %Y at %I:%M %p')}\n\n" f"We'll confirm your booking shortly. Thank you!" ), ) mail.send(msg) except Exception as exc: logger.error("Booking confirmation email failed: %s", exc) confirmed = True return render_template( "tenant/booking/form.html", tenant=tenant, services=services, staff_list=staff_list, confirmed=confirmed, error=error, )