05/07 Phase 3: initial codes
This commit is contained in:
@@ -1,7 +1,128 @@
|
||||
"""
|
||||
app/tenant/booking/routes.py
|
||||
Phase 3+ implementation.
|
||||
Public online customer booking — /book/<tenant_slug>
|
||||
No authentication required.
|
||||
"""
|
||||
from flask import Blueprint
|
||||
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
|
||||
|
||||
booking_bp = Blueprint("booking", __name__, url_prefix="")
|
||||
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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user