diff --git a/docs/bookings.md b/docs/bookings.md index df14963..986b13d 100644 --- a/docs/bookings.md +++ b/docs/bookings.md @@ -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) @@ -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 diff --git a/docs/errors.md b/docs/errors.md index fd82100..b8ec810 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -15,9 +15,9 @@ Response shape: ```json { "error": { - "code": "VALIDATION_ERROR", - "message": "Invalid request data", - "details": { ... } + "code": "invalid_input", + "message": "Invalid input", + "issues": [...] } } ``` @@ -35,25 +35,44 @@ 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. @@ -61,8 +80,25 @@ 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" } } ``` @@ -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" + } } } ``` @@ -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" } } ``` @@ -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