# API Webhooks: default vehicle import

Reference for the default inbound stock-notification webhook, based on the
`Default vehicle (1:1)` template (`DefaultVehicleNotificationTemplateSeed`). Most payload
keys map straight onto the matching vehicle column. Taxonomy fields and the dealership
are sent as name strings and resolved to ids; equipment and technical data are sent as
nested lists. Mapping/coercion is handled by `App\NotificationMaps\NotificationPayloadMapper`;
resolution and syncing by `App\NotificationMaps\NotificationMapVehicleUpsert`; the schema
shape mirrors `App\NotificationMaps\NotificationOpenApiGenerator`.

## Endpoint

```
POST /api/notifications/{tenant}/{slug}
Authorization: Bearer <token>
Content-Type: application/json
```

- `{tenant}` is the tenant identifier; `{slug}` is the webhook's slug.
- The Bearer token is an API user token scoped to this site (Settings > API Webhooks >
  "View API access token").
- Live spec: `GET /api/notifications/{tenant}/{slug}/docs` returns the OpenAPI 3.0
  document generated from the actual map (resolves even while the map is inactive).

## JSON Schema (request body)

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Default vehicle (1:1) inbound notification",
  "type": "object",
  "required": ["uuid", "type"],
  "additionalProperties": true,
  "properties": {
    "stock_provider": { "type": "string" },
    "uuid": { "type": "string", "description": "External stock identifier; dedupe/upsert key." },
    "vin": { "type": "string" },
    "cap_id": { "type": "string" },
    "cap_code": { "type": "string" },
    "type": { "type": "string", "description": "Required to create a vehicle." },
    "title": { "type": "string" },
    "derivative": { "type": "string" },
    "registration_number": { "type": "string" },
    "colour": { "type": "string" },
    "trim": { "type": "string" },
    "description": { "type": "string" },
    "attention_grabber": { "type": "string" },
    "monthly_cost_type": { "type": "string" },
    "main_video_url": { "type": "string" },
    "exterior_video_url": { "type": "string" },
    "interior_video_url": { "type": "string" },

    "make": { "type": "string", "description": "Make name; resolved to make_id via taxonomy mapping." },
    "model": { "type": "string", "description": "Model name; resolved to model_id, scoped to the resolved make." },
    "fuel_type": { "type": "string", "description": "Resolved to fuel_type_id via taxonomy mapping." },
    "body_style": { "type": "string", "description": "Resolved to body_style_id via taxonomy mapping." },
    "transmission": { "type": "string", "description": "Resolved to transmission_id via taxonomy mapping." },
    "drivetrain": { "type": "string", "description": "Resolved to drivetrain_id via taxonomy mapping." },
    "dealership": { "type": "string", "description": "Dealership name or stock location; resolved to dealership_id." },

    "door_count": { "type": "integer" },
    "seats": { "type": "integer" },
    "manufacture_year": { "type": "integer" },
    "odometer_km": { "type": "integer" },
    "odometer_mi": { "type": "integer" },
    "engine_size_cc": { "type": "integer" },
    "previous_owner_count": { "type": "integer" },

    "price": { "type": "number" },
    "original_price": { "type": "number" },
    "previous_price": { "type": "number" },
    "monthly_price": { "type": "number" },
    "rrp_price": { "type": "number" },
    "deposit": { "type": "number" },
    "admin_fee": { "type": "number" },
    "co2": { "type": "number" },
    "mpg": { "type": "number" },

    "is_published": { "type": "boolean" },
    "is_new": { "type": "boolean" },
    "is_sold": { "type": "boolean" },
    "is_reserved": { "type": "boolean" },
    "is_demo": { "type": "boolean" },
    "is_vat_applicable": { "type": "boolean" },
    "is_vat_qualifying": { "type": "boolean" },
    "featured": { "type": "boolean" },

    "first_registration_date": { "type": "string", "format": "date-time" },
    "published_at": { "type": "string", "format": "date-time" },
    "sold_at": { "type": "string", "format": "date-time" },
    "reserved_at": { "type": "string", "format": "date-time" },
    "available_date": { "type": "string", "format": "date-time" },
    "stock_arrival_date": { "type": "string", "format": "date-time" },

    "images": {
      "type": "array",
      "items": {
        "oneOf": [
          { "type": "string", "format": "uri" },
          { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } } }
        ]
      }
    },
    "equipment": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["description"],
        "properties": {
          "type": { "type": "string" },
          "description": { "type": "string" },
          "category": { "type": "string" },
          "code": { "type": "string" },
          "price": { "type": "string" }
        }
      }
    },
    "technical_data": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["description"],
        "properties": {
          "description": { "type": "string" },
          "value": { "type": "string" },
          "category": { "type": "string" },
          "code": { "type": "string" }
        }
      }
    }
  }
}
```

## Example valid payload

```json
{
  "uuid": "STK-000123",
  "type": "used",
  "stock_provider": "acme-feed",
  "registration_number": "AB12 CDE",
  "vin": "WVWZZZ1KZAW000123",
  "title": "Volkswagen Golf 1.5 TSI Life",
  "make": "Volkswagen",
  "model": "Golf",
  "fuel_type": "Petrol",
  "body_style": "Hatchback",
  "transmission": "Manual",
  "drivetrain": "Front Wheel Drive",
  "dealership": "Leeds Branch",
  "manufacture_year": 2021,
  "odometer_mi": 18450,
  "price": 18995.00,
  "monthly_price": 289.99,
  "is_published": true,
  "is_new": false,
  "first_registration_date": "2021-03-01",
  "images": [
    "https://cdn.example.com/veh/000123/1.jpg",
    { "url": "https://cdn.example.com/veh/000123/2.jpg" }
  ],
  "equipment": [
    { "type": "Standard", "description": "Air conditioning", "category": "Climate", "code": "AC" },
    { "type": "Optional", "description": "Panoramic sunroof", "category": "Exterior", "price": "950" }
  ],
  "technical_data": [
    { "description": "0-62 mph", "value": "8.5s", "category": "Performance", "code": "ACC" },
    { "description": "Boot capacity", "value": "380 litres", "category": "Dimensions" }
  ]
}
```

## Default validation rules

Rules run against the mapped output; a failure returns `422`. The default template
defines only:

| Field  | Rules               |
| ------ | ------------------- |
| `uuid` | `required`, `string` |
| `type` | `required`, `string` |

Every other field is optional.

## Types and coercion

| `field_map` type | JSON type              | Accepts / coercion                                             | Invalid value becomes |
| ---------------- | ---------------------- | ------------------------------------------------------------- | --------------------- |
| `string`         | `string`               | any scalar, trimmed                                           | `null`                |
| `integer`        | `integer`              | numeric or numeric string, cast to int                       | `null`                |
| `float`          | `number`               | numeric or numeric string, cast to float                     | `null`                |
| `boolean`        | `boolean`              | `true/false`, `"true"/"false"`, `1/0`, `"yes"/"no"`, `"on"/"off"` | `null`            |
| `date`           | `string` (`date-time`) | anything `Carbon::parse` accepts; stored as `Y-m-d H:i:s`     | `null`                |
| `image_list`     | `array`                | URL strings or `{ "url": "..." }` objects; trimmed, de-duplicated | `[]`             |
| `taxonomy`       | `string`               | a name; resolved to the record id via the taxonomy mapping system | id left unchanged |
| `dealership`     | `string`               | a name or stock location; resolved to `dealership_id`        | id left unchanged     |
| `equipment`      | `array`                | objects (`type`, `description`, `category`, `code`, `price`); `description` required | see below |
| `technical_data` | `array`                | objects (`description`, `value`, `category`, `code`); `description` required | see below     |

## Taxonomy resolution

`make`, `model`, `fuel_type`, `body_style`, `transmission` and `drivetrain` are sent as
name strings and resolved to their record ids through the shared taxonomy mapping system
(the same one import maps use):

- Resolution is namespaced per site + webhook (`{tenant}-notification-map-{id}`).
- `make` is resolved first; `model` is then resolved scoped to that make.
- A name that cannot be matched leaves the existing id untouched and is recorded as an
  unmapped term linked to the vehicle. An admin resolves it later (taxonomy mapping UI),
  which back-fills the id on all affected vehicles.

## Dealership resolution

`dealership` is matched against `dealerships.location_stock` first, then `name`. An
unmatched value leaves the existing dealership on the vehicle untouched.

## Equipment and technical data

- Sent as arrays of objects. Entries without a `description` are dropped.
- **Full replace when present:** if the `equipment` (or `technical_data`) key is in the
  payload, the vehicle's existing rows for that relation are deleted and rebuilt from the
  list, so the payload is authoritative. Send the complete list each time.
- **Omit to preserve:** if the key is absent from the payload, existing rows are left
  untouched. (A present-but-empty list therefore clears the relation.)
- `equipment.type` defaults to `Standard` when omitted.

## Behaviour

- **Dedupe/upsert key:** `uuid` (the async drain falls back to `registration_number`
  then `vin` for its dedupe reference).
- **Partial updates are safe:** a mapped source key absent from the payload leaves that
  vehicle column untouched, so omitting a field never blanks existing data.
- **Unknown keys are ignored:** anything not in the field map is silently dropped, which
  is why the schema does not forbid extra properties.
- **`cap_id` / `cap_code` are strings** even though they can look numeric.
- **Responses:** `200` received, `401` missing/invalid token, `404` unknown/inactive
  slug, `422` validation failure.
