Files
MyPOS/app/tenant/booking/routes.py
T
2026-05-07 12:17:19 -04:00

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,
)