# Optimistic locking

Mutations on existing bookings use **optimistic concurrency control** keyed by
a monotonically-increasing `version` integer. The current version travels as
an `ETag` on reads; PATCH callers must echo it back on `If-Match`.

## How it works

1. `GET /v1/bookings/:uid` returns the booking and an `ETag` header:

   ```
   ETag: "3"
   ```

   The number is the booking's current `version`. It also appears in the
   response body as `data.version`.

2. `PATCH /v1/bookings/:uid` requires that ETag on `If-Match`:

   ```
   If-Match: "3"
   ```

3. If the booking's current version is still `3`, the patch applies and the
   version is bumped to `4`. The response carries `ETag: "4"`.

4. If someone else mutated the booking in between (so the current version is
   now `4`), the server returns **`409 version_conflict`** and your patch is
   not applied:

   ```json
   {
     "error": {
       "code": "version_conflict",
       "message": "The If-Match version does not match the current booking version",
       "details": {},
       "request_id": "req_…"
     }
   }
   ```

   Re-`GET` the booking, apply your change on top of the fresh version, and
   retry.

## Error matrix

| Condition | HTTP | Code |
|---|---|---|
| `If-Match` missing | `428 Precondition Required` | `missing_if_match` |
| `If-Match` not a valid integer | `400 Bad Request` | `invalid_if_match` |
| Version no longer current | `409 Conflict` | `version_conflict` |

Weak ETags are accepted — `If-Match: W/"3"` is treated the same as
`If-Match: "3"`. The version starts at `1` on creation and increments on every
successful `PATCH`, `cancel`, or `reschedule`.

## What about cancel and reschedule?

`POST /v1/bookings/:uid/cancel` and `POST /v1/bookings/:uid/reschedule` do
**not** require `If-Match` — they are idempotent operations whose intent is
unambiguous regardless of intervening edits. They still bump the version, so a
later PATCH after a cancel must use the post-cancel ETag.

## Example: read-modify-write

```bash
# 1) GET to learn the current version
curl -i -H "Authorization: Bearer $TOKEN" \
  https://api.42min.us/v1/bookings/abc-def-…
# ETag: "3"
# { "data": { "uid": "abc-def-…", "version": 3, "metadata": { "crm_id": "C-7" }, … } }

# 2) PATCH with that ETag
curl -X PATCH https://api.42min.us/v1/bookings/abc-def-… \
  -H "Authorization: Bearer $TOKEN" \
  -H "If-Match: \"3\"" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"metadata":{"crm_id":"C-8","stage":"qualified"}}'
# → 200, ETag: "4"
```

## Don'ts

- Don't try to PATCH `start`, `end`, `timezone`, `status`, `event_type_id`,
  `uid`, or `version` — those return **`422 field_immutable`** regardless of
  the ETag. To move a booking, use **reschedule**; to end it, use **cancel**.
- Don't cache an ETag indefinitely. The version may bump from webhooks,
  workflows, or the dashboard at any moment.
- Don't try to compare ETags — they are integers, but treat them as opaque
  tokens. The only safe operation is to echo them back on `If-Match`.
