Availability engine + booking API (create/cancel/reschedule) #16

Closed
opened 2026-07-23 14:07:57 +02:00 by mivanchenko · 1 comment
Owner

Parent

#14

What to build

The availability engine and booking API, exercised headlessly (via the Flask test client / HTTP,
no browser UI yet). This is the ticket that makes "can a customer actually get booked, without
double-booking, respecting the business's rules" true end to end at the API layer.

  • Slot generation: given a client's resource opening hours, a service's duration_minutes, and
    the resource's min_notice_minutes / max_advance_days / buffer_minutes, compute the list of
    bookable start times for a given date range. Must be correct across a DST transition date
    (Europe/Berlin).
  • Booking creation endpoint: validates the requested slot is still available, creates the booking
    via the ticket-1 data-access layer, sets status to confirmed or pending based on the client's
    auto_confirm setting.
  • Concurrency: two near-simultaneous booking requests for the same resource/overlapping time — only
    one succeeds; the other gets a clean, specific error (not a 500), surfaced by the EXCLUDE
    constraint from ticket 1.
  • Cancel action: given a valid signed token (PyJWT, embedding booking_id + expiry), marks the
    booking cancelled.
  • Reschedule action: given a valid signed token, updates start_time/end_time on the existing
    booking (still protected by the EXCLUDE constraint) — implemented as a first-class update, not
    cancel-then-rebook. This action is written so it can be reused by both the customer-facing flow
    (this ticket) and the owner dashboard (ticket 6) later.
  • Token minting: booking creation returns (or the confirmation step generates) the signed
    cancel/reschedule token to be used by ticket 3/4's UI and ticket 4's email.

Acceptance criteria

  • Given a client's opening hours + a service duration, the generated slot list matches
    hand-computed expected slots for a normal day and for a day either side of a DST transition.
  • Slots inside min_notice_minutes of "now" are excluded; slots beyond max_advance_days are
    excluded; slots that would violate buffer_minutes against an existing booking are excluded.
  • Booking creation on a client with auto_confirm = true yields status confirmed; on a
    client with auto_confirm = false yields status pending.
  • Two concurrent requests for the same resource/overlapping slot: exactly one booking is
    created; the failing request gets a clean 4xx response, not a 500.
  • A valid cancel token cancels the correct booking; an expired or already-used token is
    rejected.
  • A valid reschedule token updates the booking's time and still respects the EXCLUDE
    constraint (rescheduling into an already-occupied slot fails cleanly).

Blocked by

  • #15 (Booking schema + tenancy-safe data access layer)
## Parent #14 ## What to build The availability engine and booking API, exercised headlessly (via the Flask test client / HTTP, no browser UI yet). This is the ticket that makes "can a customer actually get booked, without double-booking, respecting the business's rules" true end to end at the API layer. - Slot generation: given a client's resource opening hours, a service's `duration_minutes`, and the resource's `min_notice_minutes` / `max_advance_days` / `buffer_minutes`, compute the list of bookable start times for a given date range. Must be correct across a DST transition date (Europe/Berlin). - Booking creation endpoint: validates the requested slot is still available, creates the booking via the ticket-1 data-access layer, sets status to `confirmed` or `pending` based on the client's `auto_confirm` setting. - Concurrency: two near-simultaneous booking requests for the same resource/overlapping time — only one succeeds; the other gets a clean, specific error (not a 500), surfaced by the `EXCLUDE` constraint from ticket 1. - Cancel action: given a valid signed token (PyJWT, embedding `booking_id` + expiry), marks the booking cancelled. - Reschedule action: given a valid signed token, updates `start_time`/`end_time` on the existing booking (still protected by the `EXCLUDE` constraint) — implemented as a first-class update, not cancel-then-rebook. This action is written so it can be reused by both the customer-facing flow (this ticket) and the owner dashboard (ticket 6) later. - Token minting: booking creation returns (or the confirmation step generates) the signed cancel/reschedule token to be used by ticket 3/4's UI and ticket 4's email. ## Acceptance criteria - [ ] Given a client's opening hours + a service duration, the generated slot list matches hand-computed expected slots for a normal day and for a day either side of a DST transition. - [ ] Slots inside `min_notice_minutes` of "now" are excluded; slots beyond `max_advance_days` are excluded; slots that would violate `buffer_minutes` against an existing booking are excluded. - [ ] Booking creation on a client with `auto_confirm = true` yields status `confirmed`; on a client with `auto_confirm = false` yields status `pending`. - [ ] Two concurrent requests for the same resource/overlapping slot: exactly one booking is created; the failing request gets a clean 4xx response, not a 500. - [ ] A valid cancel token cancels the correct booking; an expired or already-used token is rejected. - [ ] A valid reschedule token updates the booking's time and still respects the `EXCLUDE` constraint (rescheduling into an already-occupied slot fails cleanly). ## Blocked by - #15 (Booking schema + tenancy-safe data access layer)
mivanchenko added the bookingready-for-agentenhancement labels 2026-07-23 14:07:57 +02:00
mivanchenko added a new dependency 2026-07-23 14:09:05 +02:00
mivanchenko added a new dependency 2026-07-23 14:09:16 +02:00
mivanchenko added a new dependency 2026-07-23 14:09:17 +02:00
Author
Owner

Implemented in 2f6e0c1.

  • Schema prerequisite: resources gains min_notice_minutes/max_advance_days/buffer_minutes; new resource_hours table (per-weekday opening hours) -- #15 didn't add this config even though #16 depends on it, so it's included here.
  • availability.py: pure slot-generation function (no I/O). Tested against hand-computed slots for a normal day and both sides of the 2026 Europe/Berlin DST transitions (spring-forward and fall-back), plus min-notice/max-advance/buffer exclusion.
  • booking_api.py: new JSON blueprint -- GET /api/booking/slots, POST /api/booking (create; status is confirmed/pending per the client's auto_confirm), POST /api/booking/cancel, POST /api/booking/reschedule, using a signed JWT (dedicated BOOKING_TOKEN_SECRET, not shared with CRM_API_TOKEN) embedding booking_id + expiry for the manage actions.
  • Concurrency: a real two-thread test (not just sequential requests) against the same overlapping slot caught a genuine gap -- Postgres can raise DeadlockDetected instead of ExclusionViolation when two inserts race the exclusion constraint directly, which would have surfaced as an uncaught 500. Both are now caught and turned into a clean 409.
  • Also fixed while implementing: reschedule was extracting the target business day from the request's raw UTC offset instead of the client's timezone (could pick the wrong day's hours/bookings near local midnight for non-daytime resources).

All acceptance criteria met. Closing.

Implemented in 2f6e0c1. - **Schema prerequisite**: `resources` gains `min_notice_minutes`/`max_advance_days`/`buffer_minutes`; new `resource_hours` table (per-weekday opening hours) -- #15 didn't add this config even though #16 depends on it, so it's included here. - **`availability.py`**: pure slot-generation function (no I/O). Tested against hand-computed slots for a normal day and both sides of the 2026 Europe/Berlin DST transitions (spring-forward and fall-back), plus min-notice/max-advance/buffer exclusion. - **`booking_api.py`**: new JSON blueprint -- `GET /api/booking/slots`, `POST /api/booking` (create; status is `confirmed`/`pending` per the client's `auto_confirm`), `POST /api/booking/cancel`, `POST /api/booking/reschedule`, using a signed JWT (dedicated `BOOKING_TOKEN_SECRET`, not shared with `CRM_API_TOKEN`) embedding `booking_id` + expiry for the manage actions. - **Concurrency**: a real two-thread test (not just sequential requests) against the same overlapping slot caught a genuine gap -- Postgres can raise `DeadlockDetected` instead of `ExclusionViolation` when two inserts race the exclusion constraint directly, which would have surfaced as an uncaught 500. Both are now caught and turned into a clean 409. - Also fixed while implementing: reschedule was extracting the target business day from the request's raw UTC offset instead of the client's timezone (could pick the wrong day's hours/bookings near local midnight for non-daytime resources). All acceptance criteria met. Closing.
Sign in to join this conversation.