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

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