# Booking-related public API (v1)

This document describes the **HTTP JSON API** used to get fare estimates and create bookings. Routes are defined in `routes/api.php`.

## Base URL

All v1 endpoints are prefixed with `/api/v1` (Laravel’s default `api` prefix + `v1`).

| Environment | Example base |
|-------------|----------------|
| Local (XAMPP) | `http://localhost/ExaDispatch/public/api/v1` |
| Production | `https://your-domain.com/api/v1` |

Adjust the path if your web server does not use `/public` in the URL (e.g. document root points at `public/`).

---

## Authentication

### Public booking API (v1)

| Header | Required | Description |
|--------|----------|-------------|
| `X-API-Key` | Yes | Valid API key string |
| `Content-Type` | For POST bodies | `application/json` |
| `Accept` | Recommended | `application/json` |

- Missing key: **401** — `{ "message": "Missing API key. Provide X-API-Key header." }`
- Invalid/expired key: **401** — `{ "message": "Invalid API key." }` or `"API key has expired."`

Create keys on the server with:

```bash
php artisan api-key:create
```

### Other API routes (not v1 booking)

- `POST /api/tokens` — email/password → Laravel Sanctum **Bearer** token (for `/api/user`, driver location, etc.). This is **separate** from the `X-API-Key` v1 integration.

---

## Rate limiting

v1 routes use middleware `throttle.api:60,1`: **60 requests per minute** per API key (see `App\Http\Middleware\ThrottleApiByKey`).

- **429** — `{ "message": "Too many requests. Please try again later." }`
- Response headers may include `X-RateLimit-Limit` and `X-RateLimit-Remaining`.

---

## Supporting endpoints (reference data)

Use these to populate `vehicle_type_id` and optional `zone_id` on stops.

### `GET /api/v1/zones`

Returns active zones.

**Response 200** — JSON array of:

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Zone ID |
| `name` | string | Zone name |
| `type` | string | Zone type (e.g. airport) |

---

### `GET /api/v1/vehicle-types`

Returns active vehicle types.

**Response 200** — JSON array of:

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Vehicle type ID |
| `name` | string | Display name |
| `multiplier` | number | Fare multiplier |
| `icon` | string or null | Absolute URL for the vehicle icon image (set in admin) |

---

## Fare estimate (booking-related)

### `POST /api/v1/estimate`

Calculates a **fare estimate** without creating a booking.

#### Request body (JSON)

| Field | Type | Required | Rules |
|-------|------|----------|--------|
| `stops` | array | Yes | Min **2** stops |
| `stops.*.address` | string | No* | Required if lat/lng not provided for that stop |
| `stops.*.lat` | number | No* | With `lng`, skips geocoding for that stop |
| `stops.*.lng` | number | No* | With `lat` |
| `stops.*.type` | string | Yes | `pickup` or `dropoff` |
| `scheduled_at` | string | Yes | Parseable date/time (e.g. ISO 8601) |
| `vehicle_type_id` | integer | Yes | Must exist in `vehicle_types` |

\*Each stop must be resolved to coordinates: either **`lat` + `lng`**, or a non-empty **`address`** (geocoded via Google Maps when configured).

#### Success response **200**

```json
{
  "data": {
    "total": 42.5,
    "currency": "GBP",
    "currency_symbol": "£",
    "total_distance_km": 25.4,
    "total_duration_minutes": 45,
    "vehicle_type": {
      "id": 1,
      "name": "Standard",
      "multiplier": 1,
      "icon": "https://example.com/storage/vehicle-icons/abc.png"
    },
    "legs": [
      {
        "from_address": "...",
        "to_address": "...",
        "from_zone": "Zone A",
        "to_zone": "Zone B",
        "distance_km": 12.34,
        "distance_display": "...",
        "fare": 42.5,
        "extras": null,
        "pickup_zone_fee": 0,
        "dropoff_zone_fee": 0,
        "pickup_zone_surcharge": 0,
        "dropoff_zone_surcharge": 0,
        "breakdown": "..."
      }
    ]
  }
}
```

(`extras` / fee fields may be numeric or structured depending on fare engine.)

#### Error **422** — geocoding / resolution failed

```json
{
  "message": "Could not resolve coordinates for all stops. Ensure addresses are valid or lat/lng are provided."
}
```

#### Error **422** — validation

Laravel validation errors (field messages under `errors`).

---

## Create booking

### `POST /api/v1/bookings`

Creates a **pending** booking, persists legs, dispatches driver matching and a confirmation email to `customer_email`.

#### Request body (JSON)

| Field | Type | Required | Rules / notes |
|-------|------|----------|----------------|
| `stops` | array | Yes | Min **2** stops; same resolution rules as estimate |
| `stops.*.address` | string | No* | |
| `stops.*.lat` | number | No* | |
| `stops.*.lng` | number | No* | |
| `stops.*.zone_id` | integer | No | Optional; must exist in `zones` |
| `stops.*.type` | string | Yes | `pickup` or `dropoff` |
| `scheduled_at` | string | Yes | Parseable date/time |
| `vehicle_type_id` | integer | Yes | Must exist |
| `customer_name` | string | Yes | Max 255 |
| `customer_email` | string | Yes | Valid email |
| `customer_phone` | string | Yes | Max 50 |
| `passenger_count` | integer | No | Default **1**; min 1, max 255 |
| `luggage_count` | integer | No | Default **0**; min 0, max 255 |
| `payment_mode` | string | No | Default **cash**; one of `cash`, `card`, `account` |
| `flight_number` | string | No | Max 50 |
| `notes` | string | No | Max **1000** |

\*Per stop: provide **`lat` + `lng`** or a geocodable **`address`**.

#### Success response **201** (default resource wrapper)

The booking is returned via `BookingResource`. Typical shape:

```json
{
  "data": {
    "id": 1,
    "reference": "API-XXXXXXXX",
    "status": "pending",
    "pickup_address": "...",
    "dropoff_address": "...",
    "vehicle_type_id": 1,
    "vehicle_type": {
      "id": 1,
      "name": "Standard",
      "multiplier": 1,
      "icon": "https://example.com/storage/vehicle-icons/abc.png"
    },
    "distance_km": 25.4,
    "distance_display": "...",
    "total_amount": 42.5,
    "currency": "GBP",
    "currency_symbol": "£",
    "scheduled_at": "2026-03-26T14:00:00+00:00",
    "customer_name": "...",
    "customer_email": "...",
    "customer_phone": "...",
    "passenger_count": 1,
    "luggage_count": 0,
    "payment_mode": "cash",
    "flight_number": null,
    "legs": [
      {
        "pickup_address": "...",
        "dropoff_address": "...",
        "distance_km": 12.3,
        "distance_display": "...",
        "amount": 42.5,
        "pickup_zone_fee": 0,
        "dropoff_zone_fee": 0,
        "pickup_zone_surcharge": 0,
        "dropoff_zone_surcharge": 0
      }
    ]
  }
}
```

Reference prefix for API-created bookings is **`API-`** (see `App\Http\Controllers\Api\BookingController`).

#### Error **422** — same as estimate when stops cannot be resolved

```json
{
  "message": "Could not resolve coordinates for all stops. Ensure addresses are valid or lat/lng are provided."
}
```

#### Error **422** — validation

Standard Laravel `{ "message": "...", "errors": { ... } }`.

---

## Example: `curl` (estimate)

```bash
curl -s -X POST "https://your-domain.com/api/v1/estimate" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "X-API-Key: YOUR_KEY_HERE" \
  -d '{
    "scheduled_at": "2026-03-26T15:00:00Z",
    "vehicle_type_id": 1,
    "stops": [
      { "type": "pickup", "address": "Heathrow Airport, UK" },
      { "type": "dropoff", "address": "Westminster, London, UK" }
    ]
  }'
```

---

## Example: `curl` (create booking)

```bash
curl -s -X POST "https://your-domain.com/api/v1/bookings" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "X-API-Key: YOUR_KEY_HERE" \
  -d '{
    "scheduled_at": "2026-03-26T15:00:00Z",
    "vehicle_type_id": 1,
    "customer_name": "Jane Doe",
    "customer_email": "jane@example.com",
    "customer_phone": "+447700900000",
    "passenger_count": 2,
    "luggage_count": 1,
    "payment_mode": "card",
    "stops": [
      { "type": "pickup", "address": "Heathrow Airport, UK" },
      { "type": "dropoff", "address": "Westminster, London, UK" }
    ]
  }'
```

---

## Operational notes

1. **Google Maps** — Address geocoding and distance matrices depend on `GOOGLE_MAPS_API_KEY` (and related config). If the key is missing, behaviour falls back where implemented (e.g. haversine distances); invalid addresses may still yield **422**.
2. **API booking `user_id`** — Stored as `null` for integrations (see `BookingController`).
3. **`pickup_note` / `dropoff_note`** — These exist on the **manual admin** booking flow; the **public v1** `POST /api/v1/bookings` payload does **not** currently accept them (only what `BookingRequest` validates).

---

## Changelog

Document generated from codebase (`routes/api.php`, `App\Http\Controllers\Api\*`, `App\Http\Requests\Api\*`, `App\Http\Resources\Api\*`). Update this file when routes or validation change.
