136 lines
4.4 KiB
Python
136 lines
4.4 KiB
Python
"""
|
|
app/tenant/checkin/routes.py — Customer self check-in kiosk.
|
|
Route: GET/POST /checkin/<tenant_slug>
|
|
No authentication required. CSRF-exempt. Rate-limited.
|
|
Alert delivery: 5-second polling via GET /api/v1/checkin/queue?status=waiting.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from flask import (
|
|
Blueprint, render_template, request, jsonify,
|
|
current_app, abort,
|
|
)
|
|
from app.extensions import db, limiter, csrf
|
|
from app.models.platform import Tenant
|
|
from app.models.salon import Customer, CheckinQueue, Location
|
|
from app.security import sanitise_string, validate_slug
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
checkin_bp = Blueprint("checkin", __name__)
|
|
|
|
|
|
@checkin_bp.route("/checkin/<tenant_slug>", methods=["GET", "POST"])
|
|
@csrf.exempt
|
|
@limiter.limit("20 per minute")
|
|
def kiosk(tenant_slug: str):
|
|
"""
|
|
Public kiosk page. No auth required.
|
|
Tenant slug validated against known slugs (not guessable).
|
|
Accepts: name, phone, service_requested — all other fields ignored.
|
|
Phone is sanitised and validated before profile lookup.
|
|
Page auto-resets after 10 seconds (configurable per tenant) via JS.
|
|
"""
|
|
# Validate slug format before hitting the DB
|
|
if not validate_slug(tenant_slug):
|
|
logger.warning("Kiosk: invalid slug format: %s", tenant_slug)
|
|
abort(404)
|
|
|
|
tenant = Tenant.query.filter_by(slug=tenant_slug, is_demo=False).first()
|
|
if not tenant or not tenant.is_active_status():
|
|
logger.warning("Kiosk: tenant not found or inactive: %s", tenant_slug)
|
|
abort(404)
|
|
|
|
# Resolve the primary location for this tenant
|
|
location = Location.query.filter_by(
|
|
tenant_id=tenant.id,
|
|
is_primary=True,
|
|
is_active=True,
|
|
).filter(Location.deleted_at.is_(None)).first()
|
|
|
|
if location is None:
|
|
abort(404)
|
|
|
|
confirmed = False
|
|
|
|
if request.method == "POST":
|
|
raw_name = request.form.get("customer_name", "")
|
|
raw_phone = request.form.get("customer_phone", "")
|
|
raw_service = request.form.get("service_requested", "")
|
|
|
|
name = sanitise_string(raw_name, max_length=150)
|
|
phone = sanitise_string(raw_phone, max_length=30)
|
|
service = sanitise_string(raw_service, max_length=150)
|
|
|
|
# Validate required fields
|
|
if not name or not phone:
|
|
return render_template(
|
|
"tenant/checkin/kiosk.html",
|
|
tenant=tenant,
|
|
error="Name and phone number are required.",
|
|
confirmed=False,
|
|
)
|
|
|
|
# Strip non-digit characters from phone for lookup consistency
|
|
phone_digits = "".join(filter(str.isdigit, phone))
|
|
if len(phone_digits) < 7:
|
|
return render_template(
|
|
"tenant/checkin/kiosk.html",
|
|
tenant=tenant,
|
|
error="Please enter a valid phone number.",
|
|
confirmed=False,
|
|
)
|
|
|
|
# Look up or create customer profile
|
|
customer = Customer.query.filter_by(
|
|
tenant_id=tenant.id,
|
|
phone=phone_digits,
|
|
).filter(Customer.deleted_at.is_(None)).first()
|
|
|
|
if customer is None:
|
|
customer = Customer(
|
|
tenant_id=tenant.id,
|
|
name=name,
|
|
phone=phone_digits,
|
|
)
|
|
db.session.add(customer)
|
|
db.session.flush() # get customer.id before committing
|
|
logger.info(
|
|
"Kiosk: new customer profile created for tenant %s", tenant.slug
|
|
)
|
|
|
|
# Queue the walk-in entry
|
|
entry = CheckinQueue(
|
|
tenant_id=tenant.id,
|
|
location_id=location.id,
|
|
customer_id=customer.id,
|
|
customer_name=name,
|
|
customer_phone=phone_digits,
|
|
service_requested=service or None,
|
|
status="waiting",
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
logger.info(
|
|
"Kiosk: check-in queued for tenant=%s location=%s customer=%s",
|
|
tenant.slug, location.id, customer.id,
|
|
)
|
|
confirmed = True
|
|
|
|
# Fetch services for the kiosk selector (if configured)
|
|
from app.models.salon import Service
|
|
services = Service.query.filter_by(
|
|
tenant_id=tenant.id,
|
|
is_active=True,
|
|
).filter(Service.deleted_at.is_(None)).order_by(Service.name).all()
|
|
|
|
return render_template(
|
|
"tenant/checkin/kiosk.html",
|
|
tenant=tenant,
|
|
services=services,
|
|
confirmed=confirmed,
|
|
error=None,
|
|
)
|