Owner settings: services, hours/buffer, auto-confirm, notify channel (#21)
Adds a session-authenticated /owner/settings blueprint for services CRUD (create/edit/deactivate), per-resource opening hours + min-notice/max-advance/ buffer, and client-level auto_confirm/notify_channel — all scoped to the logged-in owner's own client_id. Extends booking_db.py with the missing tenant-scoped update_service/update_resource/update_client writes, mirroring the existing update_booking allowlist pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,10 +65,18 @@ def get_resource(client_id, resource_id):
|
||||
|
||||
def set_resource_hours(client_id, resource_id, weekday, opens_at, closes_at):
|
||||
"""Upsert the opening hours for one weekday (0=Monday..6=Sunday) of a
|
||||
client's own resource. Raises UnknownResource if resource_id isn't
|
||||
client's own resource, or clear that weekday (closed all day) if either
|
||||
opens_at/closes_at is None. Raises UnknownResource if resource_id isn't
|
||||
client_id's."""
|
||||
if get_resource(client_id, resource_id) is None:
|
||||
raise UnknownResource(f"no resource {resource_id} for client {client_id}")
|
||||
if opens_at is None or closes_at is None:
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM resource_hours WHERE resource_id = %s AND weekday = %s",
|
||||
(resource_id, weekday))
|
||||
conn.commit()
|
||||
return None
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO resource_hours (resource_id, weekday, opens_at, closes_at) "
|
||||
@@ -82,6 +90,30 @@ def set_resource_hours(client_id, resource_id, weekday, opens_at, closes_at):
|
||||
return row
|
||||
|
||||
|
||||
_RESOURCE_UPDATABLE = {"name", "active", "min_notice_minutes", "max_advance_days",
|
||||
"buffer_minutes"}
|
||||
|
||||
|
||||
def update_resource(client_id, resource_id, **fields):
|
||||
"""Update a resource's own settings (min_notice/max_advance/buffer/etc.),
|
||||
scoped to client_id. Returns the updated row, or None if no such resource
|
||||
exists for this client."""
|
||||
bad = set(fields) - _RESOURCE_UPDATABLE
|
||||
if bad:
|
||||
raise ValueError(f"not updatable: {', '.join(sorted(bad))}")
|
||||
if not fields:
|
||||
return get_resource(client_id, resource_id)
|
||||
setsql = ", ".join(f"{c} = %s" for c in fields)
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"UPDATE resources SET {setsql} WHERE client_id = %s AND resource_id = %s "
|
||||
"RETURNING *",
|
||||
[*fields.values(), client_id, resource_id])
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row
|
||||
|
||||
|
||||
def get_resource_hours(client_id, resource_id):
|
||||
"""Return {weekday: (opens_at, closes_at)} for client_id's own resource
|
||||
(empty for a resource with no hours configured yet, or one that isn't
|
||||
@@ -121,6 +153,39 @@ def list_active_services(client_id):
|
||||
return _list_active("services", client_id)
|
||||
|
||||
|
||||
def list_services(client_id):
|
||||
"""All of client_id's services, active or not -- for the owner settings
|
||||
page (#21), which must show (and let the owner reactivate) deactivated
|
||||
services too, unlike the public/manual-booking pickers."""
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM services WHERE client_id = %s ORDER BY name",
|
||||
(client_id,))
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
_SERVICE_UPDATABLE = {"name", "duration_minutes", "price", "active"}
|
||||
|
||||
|
||||
def update_service(client_id, service_id, **fields):
|
||||
"""Update a service's own fields, scoped to client_id. Returns the
|
||||
updated row, or None if no such service exists for this client."""
|
||||
bad = set(fields) - _SERVICE_UPDATABLE
|
||||
if bad:
|
||||
raise ValueError(f"not updatable: {', '.join(sorted(bad))}")
|
||||
if not fields:
|
||||
return get_service(client_id, service_id)
|
||||
setsql = ", ".join(f"{c} = %s" for c in fields)
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"UPDATE services SET {setsql} WHERE client_id = %s AND service_id = %s "
|
||||
"RETURNING *",
|
||||
[*fields.values(), client_id, service_id])
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row
|
||||
|
||||
|
||||
def list_active_resources(client_id):
|
||||
return _list_active("resources", client_id)
|
||||
|
||||
@@ -256,8 +321,8 @@ def update_booking(client_id, booking_id, **fields):
|
||||
return row
|
||||
|
||||
|
||||
# ---- clients (read-only; the clients table itself is db.py's, this is just
|
||||
# the booking flow's read of its own client's config) ----
|
||||
# ---- clients (the clients table itself is db.py's; these are just the
|
||||
# booking flow's own read/write of its own client's config) ----
|
||||
|
||||
def get_client(client_id):
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
@@ -275,6 +340,30 @@ def get_client_by_slug(slug):
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
_CLIENT_UPDATABLE = {"auto_confirm", "notify_channel"}
|
||||
|
||||
|
||||
def update_client(client_id, **fields):
|
||||
"""Update a client's own booking settings (auto_confirm/notify_channel),
|
||||
for the owner settings page (#21). The only booking-flow write to
|
||||
clients -- everything else about a client is the CRM operator's via
|
||||
db.py/app.py. Returns the updated row, or None if client_id is
|
||||
unknown."""
|
||||
bad = set(fields) - _CLIENT_UPDATABLE
|
||||
if bad:
|
||||
raise ValueError(f"not updatable: {', '.join(sorted(bad))}")
|
||||
if not fields:
|
||||
return get_client(client_id)
|
||||
setsql = ", ".join(f"{c} = %s" for c in fields)
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"UPDATE clients SET {setsql} WHERE client_id = %s RETURNING *",
|
||||
[*fields.values(), client_id])
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row
|
||||
|
||||
|
||||
# ---- users (owner login) ----
|
||||
|
||||
def create_user(client_id, email, password):
|
||||
|
||||
Reference in New Issue
Block a user