Booking schema + tenancy-safe data access layer #15
Notifications
Due Date
No due date set.
Blocks
#16 Availability engine + booking API (create/cancel/reschedule)
BPPP/smb-online
#22 Per-client ICS calendar feed
BPPP/smb-online
#24 New-client onboarding automation update
BPPP/smb-online
Reference: BPPP/smb-online#15
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
#14
What to build
The foundational booking schema and a single tenancy-safe data-access module. This ticket has no
user-facing UI — it's the plumbing every other booking ticket builds on:
resources,services,and a
bookingstable wired for correct double-booking protection, plus theusers/password_reset_tokenstables and theclientscolumn additions the rest of the module needs.resourcestable (resource_idpk,client_id,name,active). One row per bookableunit (staff/chair); single-provider clients get exactly one row, but the schema doesn't assume
single-resource.
servicestable (service_idpk,client_id,name,duration_minutes,price,active).bookingsgains aresource_idforeign key and aduringgeneratedtstzrangecolumn (fromstart_time/end_time) with a PostgresEXCLUDE USING gist (resource_id WITH =, during WITH &&)constraint — the single source of truth for "no double-booking."bookings.statusgainspendingas a valid value.userstable (user_idpk,client_id,emailunique,password_hash, timestamps).password_reset_tokenstable (token,user_id,expires_at,used_at).clientsgains:slug(unique, public URL identifier),timezone(default'Europe/Berlin'),auto_confirm(boolean, default true),ics_token(per-client secret for the calendar feed).requires (or resolves from session/token)
client_idand injects the filter itself — nothingabove this layer ever writes
WHERE client_id = ...by hand. Functions needed at minimum:create/read resource, create/read service, create booking (respecting the exclusion constraint),
read bookings for a client, update booking (for the reschedule path later), create user, read
user by email scoped to client, create/consume password reset token.
Acceptance criteria
changes in this repo are applied (
backoffice/db/init.sqlconvention).EXCLUDEconstraint rejects anINSERTof an overlapping booking for the sameresource_id(proven by a test, not just by inspection).client_idcannot read or write clientB's resources/services/bookings/users, even when IDs are guessed.
tables (this ticket doesn't need to add routes — just the module and its tests).
password_reset_tokenssupports single-use semantics (consuming a token invalidates it).Blocked by
None — can start immediately.
Implemented in
b5c0fc8.backoffice/db/init.sql):resources,services,users,password_reset_tokenstables;bookingsgainsresource_id+ a generatedduring tstzrangecolumn with abtree_gistEXCLUDE USING gist (resource_id WITH =, during WITH &&)constraint (double-booking guard, proven by test);clientsgainsslug,timezone,auto_confirm,ics_token. Verifiedinit.sqlre-applies cleanly on top of the existing production schema (had to switch the trigger-install loop toCREATE OR REPLACE TRIGGERso it doesn't abort before reaching the new tables' triggers on redeploy).backoffice/app/booking_db.py): the only place raw SQL runs against these tables. Every function takesclient_idand injects the tenant filter itself.create_booking/update_bookingalso verifyresource_idbelongs to thatclient_idbefore writing, so a guessed cross-tenant resource_id is rejected (UnknownResource), not silently accepted.backoffice/app/tests/, 23 tests): run against a real throwaway Postgres 16 container (matching prod), no mocking, per #14's testing decision -- exercise the exclusion constraint, cross-tenant read/write isolation (including guessed IDs), and password-reset single-use semantics.All acceptance criteria met. Closing.