129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
"""
|
|
app/tenant/booking/routes.py
|
|
Public online customer booking — /book/<tenant_slug>
|
|
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/<tenant_slug>", 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,
|
|
)
|