103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
"""
|
|
app/tenant/checkin/routes.py
|
|
Customer self check-in kiosk — /checkin/<tenant_slug>
|
|
No authentication required. CSRF-exempt. Rate-limited.
|
|
Receptionist alert delivered via 5-second polling: GET /api/v1/checkin/queue?status=waiting
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from flask import Blueprint, render_template, request, redirect, url_for, g
|
|
from app.extensions import db, limiter, csrf
|
|
from app.models.platform import Tenant
|
|
from app.models.salon import CheckinQueue, Customer, Service, Location
|
|
|
|
logger = logging.getLogger(__name__)
|
|
checkin_bp = Blueprint("checkin", __name__)
|
|
|
|
|
|
@checkin_bp.route("/checkin/<tenant_slug>", methods=["GET", "POST"])
|
|
@limiter.limit("20 per minute")
|
|
@csrf.exempt
|
|
def kiosk(tenant_slug):
|
|
# Validate slug against known tenants — prevents enumeration
|
|
tenant = Tenant.query.filter_by(slug=tenant_slug).filter(
|
|
Tenant.status.in_(["active", "trial"])
|
|
).first()
|
|
if not tenant:
|
|
return render_template("tenant/checkin/not_found.html"), 404
|
|
|
|
# Resolve location: session-stored or primary
|
|
location = Location.query.filter_by(
|
|
tenant_id=tenant.id, is_primary=True, is_active=True
|
|
).first()
|
|
if not location:
|
|
location = Location.query.filter_by(
|
|
tenant_id=tenant.id, is_active=True
|
|
).first()
|
|
if not location:
|
|
return render_template("tenant/checkin/not_found.html"), 404
|
|
|
|
services = Service.query.filter_by(
|
|
tenant_id=tenant.id, is_active=True).filter(
|
|
Service.deleted_at.is_(None)).order_by(Service.name).all()
|
|
|
|
confirmed = False
|
|
error = None
|
|
|
|
if request.method == "POST":
|
|
# Accept ONLY these three fields — all others ignored
|
|
customer_name = request.form.get("customer_name", "").strip()[:255]
|
|
customer_phone = request.form.get("customer_phone", "").strip()[:30]
|
|
service_requested = request.form.get("service_requested", "").strip()[:255] or None
|
|
|
|
if not customer_name or not customer_phone:
|
|
error = "Please enter your name and phone number."
|
|
else:
|
|
# Look up or create customer profile
|
|
customer = Customer.query.filter_by(
|
|
tenant_id=tenant.id, phone=customer_phone
|
|
).filter(Customer.deleted_at.is_(None)).first()
|
|
customer_id = None
|
|
if customer:
|
|
customer_id = customer.id
|
|
else:
|
|
# Auto-create profile
|
|
new_cust = Customer(
|
|
tenant_id=tenant.id,
|
|
name=customer_name,
|
|
phone=customer_phone,
|
|
is_active=True,
|
|
loyalty_points=0,
|
|
no_show_count=0,
|
|
)
|
|
db.session.add(new_cust)
|
|
db.session.flush()
|
|
customer_id = new_cust.id
|
|
logger.info("Kiosk: new customer profile created tenant=%s phone=%s",
|
|
tenant.id, customer_phone)
|
|
|
|
entry = CheckinQueue(
|
|
tenant_id=tenant.id,
|
|
location_id=location.id,
|
|
customer_id=customer_id,
|
|
customer_name=customer_name,
|
|
customer_phone=customer_phone,
|
|
service_requested=service_requested,
|
|
checked_in_at=datetime.now(timezone.utc),
|
|
status="waiting",
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
logger.info("Kiosk check-in: tenant=%s location=%s customer=%s service=%s",
|
|
tenant.id, location.id, customer_id, service_requested)
|
|
confirmed = True
|
|
|
|
return render_template(
|
|
"tenant/checkin/kiosk.html",
|
|
tenant=tenant,
|
|
services=services,
|
|
confirmed=confirmed,
|
|
error=error,
|
|
)
|