Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/bookings.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ This:
- Sets `status = confirmed`
- Clears `expiresAt` on both the booking and its allocations

Confirm is safe to retry — confirming an already confirmed booking returns the confirmed booking.
Returns `409 Conflict` with code `booking.hold_expired` if the hold expired before confirmation, or `booking.invalid_transition` if the booking is not in `hold` state.

Returns `409 Conflict` with code `hold_expired` if the hold expired before confirmation.
Confirm is safe to retry with the same `Idempotency-Key` header.

## Cancel (release)

Expand All @@ -105,7 +105,9 @@ This:
- Sets `status = canceled`
- Deactivates allocations (`active = false`)

Cancel works on both `hold` and `confirmed` bookings. It's safe to retry — canceling an already canceled booking returns the booking.
Cancel works on both `hold` and `confirmed` bookings. Returns `409 Conflict` with code `booking.invalid_transition` if the booking is already `canceled` or `expired`.

Cancel is safe to retry with the same `Idempotency-Key` header.

## Expiration

Expand Down
120 changes: 87 additions & 33 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Response shape:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": { ... }
"code": "invalid_input",
"message": "Invalid input",
"issues": [...]
}
}
```
Expand All @@ -35,34 +35,70 @@ Another active allocation already blocks that time range on that resource.
```json
{
"error": {
"code": "overlap_conflict",
"message": "Time slot conflicts with existing allocation"
"code": "allocation.overlap",
"message": "Conflict",
"details": {
"conflictingAllocationIds": ["alc_..."]
}
}
}
```

### Policy rejected

The service's policy rejects the booking request (wrong hours, invalid duration, etc.).
The service's policy rejects the booking request (wrong hours, invalid duration, etc.). The top-level code is always `policy.rejected`; the specific reason is in `details.code`.

```json
{
"error": {
"code": "policy_rejected",
"message": "Policy evaluation failed"
"code": "policy.rejected",
"message": "Conflict",
"details": {
"code": "policy.blackout",
"message": "Blackout window"
}
}
}
```

Policy sub-codes:

| `details.code` | Meaning |
| -------------------------------- | ------------------------------------------- |
| `policy.blackout` | Date falls in a blackout window |
| `policy.closed` | Service is closed at that time |
| `policy.invalid_duration` | Duration not allowed by policy |
| `policy.misaligned_start` | Start time doesn't align to scheduling grid |
| `policy.lead_time_violation` | Too short notice |
| `policy.horizon_exceeded` | Too far in advance |
| `policy.overnight_not_supported` | Overnight bookings not supported |

### Resource not in service

The requested resource does not belong to the specified service.

```json
{
"error": {
"code": "resource_not_in_service",
"message": "Resource does not belong to this service"
"code": "service.resource_not_member",
"message": "Conflict",
"details": {
"serviceId": "svc_...",
"resourceId": "rsc_..."
}
}
}
```

### Service has no policy

Trying to create a booking against a service that has no policy attached.

```json
{
"error": {
"code": "service.no_policy",
"message": "Conflict"
}
}
```
Expand All @@ -74,21 +110,29 @@ Trying to confirm a booking whose hold has already expired.
```json
{
"error": {
"code": "hold_expired",
"message": "Hold expired"
"code": "booking.hold_expired",
"message": "Conflict",
"details": {
"expiresAt": "2026-03-01T10:15:00.000Z",
"serverTime": "2026-03-01T10:20:00.000Z"
}
}
}
```

### Invalid state transition

Trying to confirm a booking that is not in `hold` state.
Trying to confirm or cancel a booking that is not in the expected state.

```json
{
"error": {
"code": "invalid_state_transition",
"message": "Cannot transition from current status"
"code": "booking.invalid_transition",
"message": "Conflict",
"details": {
"currentStatus": "confirmed",
"requestedStatus": "confirmed"
}
}
}
```
Expand All @@ -113,60 +157,69 @@ Trying to delete a resource that has allocations or service associations.
```json
{
"error": {
"code": "resource_in_use",
"message": "Resource has active allocations or service associations"
"code": "resource.in_use",
"message": "Conflict",
"details": {
"message": "Resource has active allocations or service associations"
}
}
}
```

### Service has no policy
### Policy in use

Trying to create a booking against a service that has no policy attached.
Trying to delete a policy that is referenced by one or more services or bookings.

```json
{
"error": {
"code": "service.no_policy",
"message": "Service must have a policy to create bookings"
"code": "policy.in_use",
"message": "Conflict",
"details": {
"message": "Policy is referenced by one or more services or bookings"
}
}
}
```

### Policy in use
### Booking-owned allocation

Trying to delete a policy that is referenced by one or more services or bookings.
Trying to delete an allocation that belongs to a booking. Use the booking cancel endpoint instead.

```json
{
"error": {
"code": "policy.in_use",
"message": "Policy is referenced by one or more services or bookings"
"code": "allocation.managed_by_booking",
"message": "Conflict",
"details": {
"bookingId": "bkg_..."
}
}
}
```

### Booking-owned allocation
### Idempotency mismatch

Trying to delete an allocation that belongs to a booking. Use the booking cancel endpoint instead.
Same `Idempotency-Key` header but different request body or path.

```json
{
"error": {
"code": "booking_owned_allocation",
"message": "Allocation belongs to a booking"
"code": "idempotency_payload_mismatch",
"message": "Idempotency key already used with different payload"
}
}
```

### Idempotency mismatch
## 425 Too Early

Same `Idempotency-Key` header but different request body.
A previous request with the same `Idempotency-Key` is still processing. Wait and retry.

```json
{
"error": {
"code": "IDEMPOTENCY_MISMATCH",
"message": "Idempotency key already used with different parameters"
"code": "idempotency_in_progress",
"message": "Previous request still in progress"
}
}
```
Expand All @@ -180,4 +233,5 @@ Unexpected failure. Retry with backoff and the same `Idempotency-Key` header whe
- `200`/`201` - proceed
- `409` - pick a new time slot, adjust parameters, or ask the user
- `422` - fix your payload (bug)
- `425` - wait and retry with the same `Idempotency-Key`
- `5xx` - retry with backoff + idempotency