"""Owner settings: services CRUD, per-resource opening hours/notice/buffer, auto_confirm, and notify_channel (#21). All routes are session-authenticated via owner_auth.login_required and read client_id from the session only (never a request param), so every write goes through booking_db.py's own tenant-scoped UPDATE ... WHERE client_id = ... guard on top of that. Because booking_api.py's slot generation and booking creation always re-read resources/services/clients fresh on every request (#16, #18), a settings change here is live for the public booking page and availability engine on the very next request -- no cache to invalidate. """ from datetime import time from flask import Blueprint, redirect, render_template, request, session, url_for import booking_db as bdb from owner_auth import login_required bp = Blueprint("owner_settings", __name__, url_prefix="/owner/settings") WEEKDAYS = [0, 1, 2, 3, 4, 5, 6] # 0=Monday..6=Sunday, matching resource_hours def _redirect(error=None): return redirect(url_for("owner_settings.settings", error=error)) def _parse_int(value): try: return int(value) except (TypeError, ValueError): return None def _parse_price(value): value = (value or "").strip() if not value: return None try: return float(value) except ValueError: return None def _parse_time(value): value = (value or "").strip() if not value: return None try: return time.fromisoformat(value) except ValueError: return None @bp.get("") @login_required def settings(): client_id = session["client_id"] client = bdb.get_client(client_id) resources = bdb.list_resources(client_id) hours_by_resource = { r["resource_id"]: bdb.get_resource_hours(client_id, r["resource_id"]) for r in resources} ics_token = bdb.ensure_ics_token(client_id) return render_template( "owner/settings.html", client=client, services=bdb.list_services(client_id), resources=resources, hours_by_resource=hours_by_resource, weekdays=WEEKDAYS, ics_url=url_for("bookings_ics", token=ics_token, _external=True), error=request.args.get("error")) @bp.post("/services") @login_required def create_service(): client_id = session["client_id"] name = (request.form.get("name") or "").strip() duration_minutes = _parse_int(request.form.get("duration_minutes")) price = _parse_price(request.form.get("price")) if not name or not duration_minutes or duration_minutes <= 0: return _redirect(error="invalid_service") bdb.create_service(client_id, name, duration_minutes, price=price) return _redirect() @bp.post("/services/") @login_required def update_service(service_id): client_id = session["client_id"] name = (request.form.get("name") or "").strip() duration_minutes = _parse_int(request.form.get("duration_minutes")) price = _parse_price(request.form.get("price")) active = request.form.get("active") == "on" if not name or not duration_minutes or duration_minutes <= 0: return _redirect(error="invalid_service") row = bdb.update_service( client_id, service_id, name=name, duration_minutes=duration_minutes, price=price, active=active) if row is None: return _redirect(error="not_found") return _redirect() @bp.post("/resources/") @login_required def update_resource(resource_id): client_id = session["client_id"] min_notice_minutes = _parse_int(request.form.get("min_notice_minutes")) max_advance_days = _parse_int(request.form.get("max_advance_days")) buffer_minutes = _parse_int(request.form.get("buffer_minutes")) if min_notice_minutes is None or min_notice_minutes < 0: return _redirect(error="invalid_resource") if max_advance_days is None or max_advance_days < 0: return _redirect(error="invalid_resource") if buffer_minutes is None or buffer_minutes < 0: return _redirect(error="invalid_resource") row = bdb.update_resource( client_id, resource_id, min_notice_minutes=min_notice_minutes, max_advance_days=max_advance_days, buffer_minutes=buffer_minutes) if row is None: return _redirect(error="not_found") return _redirect() @bp.post("/resources//hours") @login_required def update_resource_hours(resource_id): client_id = session["client_id"] if bdb.get_resource(client_id, resource_id) is None: return _redirect(error="not_found") # Parse and validate every weekday up front so a bad entry later in the # form (e.g. Wednesday) can't leave earlier weekdays (Monday, Tuesday) # already written -- either the whole week's hours update or none of it # does, matching update_resource/update_client's all-or-nothing shape. parsed = {} for weekday in WEEKDAYS: if request.form.get(f"closed_{weekday}") == "on": parsed[weekday] = None continue opens_at = _parse_time(request.form.get(f"opens_at_{weekday}")) closes_at = _parse_time(request.form.get(f"closes_at_{weekday}")) if opens_at is None or closes_at is None or closes_at <= opens_at: return _redirect(error="invalid_hours") parsed[weekday] = (opens_at, closes_at) for weekday, hours in parsed.items(): if hours is None: bdb.set_resource_hours(client_id, resource_id, weekday, None, None) else: bdb.set_resource_hours(client_id, resource_id, weekday, *hours) return _redirect() @bp.post("/client") @login_required def update_client(): client_id = session["client_id"] auto_confirm = request.form.get("auto_confirm") == "on" notify_channel = (request.form.get("notify_channel") or "").strip() or None if notify_channel is not None and notify_channel != "telegram": return _redirect(error="invalid_notify_channel") bdb.update_client(client_id, auto_confirm=auto_confirm, notify_channel=notify_channel) return _redirect()