|
| 1 | +--- |
| 2 | +title: Booking Policy |
| 3 | +description: How the booking-service enforces tenant configuration on every POST /v1/bookings |
| 4 | +order: 5 |
| 5 | +--- |
| 6 | + |
| 7 | +# Booking Policy Enforcement |
| 8 | + |
| 9 | +Every `POST /v1/bookings` passes through a sequential policy pipeline before the booking is stored. The pipeline reads the tenant's `booking` and `business-hours` module configs from the config-service (cached for 60 seconds) and applies seven checks in order. |
| 10 | + |
| 11 | +**If any check fails, the pipeline stops immediately and returns the error.** Subsequent checks are not evaluated. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## The Pipeline |
| 16 | + |
| 17 | +### 1. Advance Booking Limit |
| 18 | + |
| 19 | +**Config key:** `booking.advanceBookingDays` (default: 365) |
| 20 | + |
| 21 | +`slotStart` must not be further into the future than `now + advanceBookingDays`. |
| 22 | + |
| 23 | +``` |
| 24 | +slotStart > now + 14 days → 422 "slot is too far in the future: bookings may only be created up to 14 day(s) ahead" |
| 25 | +``` |
| 26 | + |
| 27 | +**Use case:** Prevent customers from reserving slots years in advance. Set to `14` for a two-week booking window. |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +### 2. Past-Slot Check |
| 32 | + |
| 33 | +No config — always enforced. |
| 34 | + |
| 35 | +`slotStart` must be in the future relative to the server's current UTC time. |
| 36 | + |
| 37 | +``` |
| 38 | +slotStart ≤ now → 422 "slot start must be in the future" |
| 39 | +``` |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +### 3. Minimum Duration |
| 44 | + |
| 45 | +**Config key:** `booking.slotDurationMinutes` (default: 30) |
| 46 | + |
| 47 | +The slot length (`slotEnd − slotStart` in minutes) must be ≥ `slotDurationMinutes`. |
| 48 | + |
| 49 | +``` |
| 50 | +(slotEnd - slotStart) < 30 min → 422 "slot duration (20 min) is shorter than the minimum slot duration configured for this tenant (30 min)" |
| 51 | +``` |
| 52 | + |
| 53 | +**Use case:** Prevent zero-length or unrealistically short bookings. |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +### 4. Business Hours |
| 58 | + |
| 59 | +**Config keys:** `business-hours` module (all fields) |
| 60 | + |
| 61 | +This check only runs when: |
| 62 | +- The tenant has **explicitly saved** a `business-hours` config (`isDefault: false`), **and** |
| 63 | +- `allowBookingsOutsideHours: false` |
| 64 | + |
| 65 | +If either condition is not met, business-hours enforcement is skipped entirely. |
| 66 | + |
| 67 | +When active, the check validates: |
| 68 | + |
| 69 | +1. The start day's `open` flag — if `false`: `"business is closed on <day>"` |
| 70 | +2. `slotStart ≥ openTime` on the start day — if violated: `"slot starts before opening time (HH:MM)"` |
| 71 | +3. `slotEnd ≤ closeTime` on the **end day** — if violated: `"slot ends after closing time (HH:MM)"` |
| 72 | +4. For same-day bookings, the slot must not overlap the configured break window |
| 73 | + |
| 74 | +For multi-day bookings, the closing time check uses the *end day's* schedule (not the start day's), so a booking that genuinely spans overnight is evaluated correctly. |
| 75 | + |
| 76 | +``` |
| 77 | +slotStart on Saturday, open: false → 422 "business is closed on saturday" |
| 78 | +slotStart before 09:00 → 422 "slot starts before opening time (09:00)" |
| 79 | +slotEnd after 17:00 → 422 "slot ends after closing time (17:00)" |
| 80 | +overlaps 12:00–13:00 break → 422 "slot overlaps with the business break (12:00 – 60 min)" |
| 81 | +``` |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +### 5. Daily Capacity |
| 86 | + |
| 87 | +**Config key:** `booking.maxBookingsPerDay` (default: 100) |
| 88 | + |
| 89 | +The count of non-cancelled, non-no-show bookings on the same UTC calendar day as `slotStart` must be less than `maxBookingsPerDay`. |
| 90 | + |
| 91 | +``` |
| 92 | +count ≥ 50 → 409 "daily booking limit reached (50/50) — no further bookings accepted on this date" |
| 93 | +``` |
| 94 | + |
| 95 | +**Use case:** Hard-cap how many bookings a tenant's operation can handle per day. |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +### 6. Buffer Gap |
| 100 | + |
| 101 | +**Config key:** `booking.bufferMinutes` (default: 0, enforcement skipped when 0) |
| 102 | + |
| 103 | +No existing booking for the same `serviceRef` may fall within ±`bufferMinutes` of the new slot. |
| 104 | + |
| 105 | +The SQL check: |
| 106 | + |
| 107 | +```sql |
| 108 | +slot_end > NEW.slotStart - (bufferMinutes * interval '1 minute') |
| 109 | +AND |
| 110 | +slot_start < NEW.slotEnd + (bufferMinutes * interval '1 minute') |
| 111 | +``` |
| 112 | + |
| 113 | +``` |
| 114 | +existing booking bk_xyz overlaps the 15-min buffer → 409 "a 15-minute buffer is required between consecutive bookings for this service" |
| 115 | +``` |
| 116 | + |
| 117 | +**Use case:** Ensure cleanup or travel time between appointments for the same service. |
| 118 | + |
| 119 | +--- |
| 120 | + |
| 121 | +### 7. Auto-Confirm |
| 122 | + |
| 123 | +**Config key:** `booking.autoConfirm` (default: false) |
| 124 | + |
| 125 | +Not a gate — this step sets the initial status. |
| 126 | + |
| 127 | +- `autoConfirm: true` → booking is stored as `status: confirmed` |
| 128 | +- `autoConfirm: false` → booking is stored as `status: pending` (operator must confirm manually) |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Graceful Degradation |
| 133 | + |
| 134 | +If the config-service is **unreachable** when a booking is created, the booking-service falls back to its in-process defaults: |
| 135 | + |
| 136 | +| Setting | Fallback value | |
| 137 | +|---|---| |
| 138 | +| `slotDurationMinutes` | 30 | |
| 139 | +| `maxBookingsPerDay` | 1000 | |
| 140 | +| `advanceBookingDays` | 365 | |
| 141 | +| `autoConfirm` | false | |
| 142 | +| `bufferMinutes` | 0 | |
| 143 | +| Business hours | Not enforced | |
| 144 | + |
| 145 | +A config-service outage will never prevent bookings from being created. The cache also means that even if the config-service goes down, the last known config remains in effect for up to 60 seconds. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## Config Propagation Timing |
| 150 | + |
| 151 | +The booking-service caches each tenant's module config for **60 seconds** (configurable via `CONFIG_CACHE_TTL` environment variable). After you `PUT /v1/config/booking`, the new rules take effect within one cache TTL cycle. |
| 152 | + |
| 153 | +To change the TTL: |
| 154 | + |
| 155 | +```yaml |
| 156 | +# deploy/docker/docker-compose.dev.yml |
| 157 | +booking-service: |
| 158 | + environment: |
| 159 | + CONFIG_CACHE_TTL: "10" # seconds |
| 160 | +``` |
| 161 | +
|
| 162 | +--- |
| 163 | +
|
| 164 | +## Disabling Enforcement for Testing |
| 165 | +
|
| 166 | +The fastest way to disable all policy checks during development is to set permissive values: |
| 167 | +
|
| 168 | +```bash |
| 169 | +# Turn off all restrictions |
| 170 | +curl -X PUT http://localhost:8085/v1/config/booking \ |
| 171 | + -H "Content-Type: application/json" \ |
| 172 | + -H "X-Tenant-ID: 01HZ..." \ |
| 173 | + -d '{ |
| 174 | + "slotDurationMinutes": 1, |
| 175 | + "maxBookingsPerDay": 99999, |
| 176 | + "advanceBookingDays": 3650, |
| 177 | + "autoConfirm": true, |
| 178 | + "bufferMinutes": 0 |
| 179 | + }' |
| 180 | + |
| 181 | +# Disable business hours |
| 182 | +curl -X PUT http://localhost:8085/v1/config/business-hours \ |
| 183 | + -H "Content-Type: application/json" \ |
| 184 | + -H "X-Tenant-ID: 01HZ..." \ |
| 185 | + -d '{"allowBookingsOutsideHours": true}' |
| 186 | +``` |
0 commit comments