# features_json Usage Report

## Scope

This report scans the project for all direct usages of `features_json` and the main derived usages that depend on it indirectly through:

- `feature_labels`
- `Vehicle::FEATURE_OPTIONS`
- Inertia/API vehicle serialization

The goal is accuracy, so this report separates:

1. Direct `features_json` references
2. Derived feature usage that depends on `features_json`
3. Non-usages that might look related but are actually part of the dynamic attribute system

## Executive Summary

### Direct `features_json` references found

Direct references exist in:

- `app/Models/Vehicle.php`
- `app/Http/Requests/StoreVehicleRequest.php`
- `app/Http/Requests/UpdateVehicleRequest.php`
- `resources/js/Components/Vehicles/VehicleForm.vue`

### Derived feature usage found

Derived usage exists in:

- `app/Models/Vehicle.php` via `feature_labels`
- `app/Http/Controllers/Admin/VehicleController.php`
- `app/Http/Controllers/Seller/VehicleController.php`
- `app/Http/Controllers/PublicController.php`
- `app/Http/Controllers/Api/VehicleController.php`
- `resources/js/Pages/Admin/Vehicles/Show.vue`
- `resources/js/Pages/Seller/Vehicles/Show.vue`
- `resources/js/Pages/Public/VehicleShow.vue`
- `resources/js/Pages/Customer/Orders/Show.vue`
- `resources/js/Pages/Admin/Vehicles/Create.vue`
- `resources/js/Pages/Vehicles/Edit.vue`
- `resources/js/Pages/Seller/Vehicles/Create.vue`
- `config/vehicle_features.php`

### No direct usages found in

- Blade files
- Routes
- Tests
- Dedicated Eloquent scopes for `features_json`
- Filter/search logic querying `features_json`

### JSON decode / parse findings

- `json_decode($this->features_json, true)` is used in both vehicle request classes
- `json_decode($features, true)` is used in `Vehicle::normalizedFeatureKeys()` as a defensive fallback
- No `JSON.parse(...)` usage for `features_json` was found in Vue
- Vue uses `JSON.stringify(...)` before submit instead

### Filtering logic findings

No filtering/search logic based on `features_json` was found.

The public “Features” filter in `resources/js/Pages/Public/Home.vue` is not powered by `features_json`. It uses boolean dynamic attributes from `attribute_filters` instead.

## Supported Feature Keys

The supported feature keys are defined in `Vehicle::FEATURE_OPTIONS` and mapped to labels in `config/vehicle_features.php`.

| Key | Label |
| --- | --- |
| `cd_player` | CD Player |
| `sunroof` | Sunroof |
| `leather_seats` | Leather Seats |
| `alloy_wheels` | Alloy Wheels |
| `power_steering` | Power Steering |
| `power_windows` | Power Windows |
| `ac` | Air Conditioning |
| `abs` | ABS |
| `airbags` | Airbags |
| `radio` | Radio |
| `cd_changer` | CD Changer |
| `dvd` | DVD |
| `tv` | TV |
| `power_seats` | Power Seats |
| `back_tire` | Back Tire |
| `grill_guard` | Grill Guard |
| `rear_spoiler` | Rear Spoiler |
| `central_locking` | Central Locking |
| `jack` | Jack |
| `spare_tire` | Spare Tire |
| `wheel_spanner` | Wheel Spanner |
| `fog_lights` | Fog Lights |
| `back_camera` | Back Camera |
| `push_start` | Push Start |
| `keyless_entry` | Keyless Entry |
| `esc` | ESC |
| `360_camera` | 360 Camera |
| `body_kit` | Body Kit |
| `side_airbags` | Side Airbags |
| `power_mirrors` | Power Mirrors |
| `side_skirts` | Side Skirts |
| `front_lip_spoiler` | Front Lip Spoiler |
| `navigation` | Navigation |
| `turbo` | Turbo |
| `power_slide_door` | Power Slide Door |
| `etc` | ETC |

## 1. Direct features_json Usages

### 1.1 app/Models/Vehicle.php

**File path**

`app/Models/Vehicle.php`

**Code snippet**

```php
protected $fillable = [
    // ...
    'features_json',
];

protected $casts = [
    'features_json' => 'array',
];
```

**Purpose of usage**

- Data storage
- API response

**Notes**

- `features_json` is mass assignable
- It is automatically cast to an array when the model is hydrated/serialized
- Because it is not hidden, it is likely exposed in API and Inertia payloads whenever a `Vehicle` model is serialized

---

### 1.2 app/Models/Vehicle.php

**File path**

`app/Models/Vehicle.php`

**Code snippet**

```php
private function normalizedFeatureKeys(): Collection
{
    $features = $this->features_json;

    if (is_string($features)) {
        $features = json_decode($features, true);
    }

    $featureCollection = collect(is_array($features) ? $features : []);
    $hasStringKeys = $featureCollection->keys()->contains(fn ($key) => ! is_int($key));

    if ($hasStringKeys) {
        return $featureCollection
            ->filter(fn ($enabled) => (bool) $enabled)
            ->keys()
            ->filter(fn ($feature) => is_string($feature) && $feature !== '');
    }

    return $featureCollection
        ->filter(fn ($feature) => is_string($feature) && $feature !== '');
}
```

**Purpose of usage**

- Display (UI)
- API response

**Notes**

- This method normalizes the stored JSON into a list of enabled feature keys
- It supports two storage shapes:
  - associative object style: `{ "abs": true, "airbags": false }`
  - array style: `["abs", "sunroof"]`
- It uses `json_decode(...)` as a defensive fallback if `features_json` reaches this method as a raw string

---

### 1.3 app/Http/Requests/StoreVehicleRequest.php

**File path**

`app/Http/Requests/StoreVehicleRequest.php`

**Code snippet**

```php
'features_json' => ['nullable', 'array'],
'features_json.*' => ['boolean'],

if (is_string($this->features_json)) {
    $decoded = json_decode($this->features_json, true);

    if (json_last_error() === JSON_ERROR_NONE) {
        $this->merge([
            'features_json' => $decoded,
        ]);
    }
}
```

**Purpose of usage**

- Validation
- Data storage

**Notes**

- Validates `features_json` as an array of boolean values
- Accepts JSON-string form input and decodes it before validation
- This is part of the create pipeline

---

### 1.4 app/Http/Requests/UpdateVehicleRequest.php

**File path**

`app/Http/Requests/UpdateVehicleRequest.php`

**Code snippet**

```php
'features_json' => ['nullable', 'array'],
'features_json.*' => ['boolean'],

if (is_string($this->features_json)) {
    $decoded = json_decode($this->features_json, true);

    if (json_last_error() === JSON_ERROR_NONE) {
        $this->merge([
            'features_json' => $decoded,
        ]);
    }
}
```

**Purpose of usage**

- Validation
- Data storage

**Notes**

- Same pattern as store
- Handles update submissions where the frontend sends `features_json` as a JSON string

---

### 1.5 resources/js/Components/Vehicles/VehicleForm.vue

**File path**

`resources/js/Components/Vehicles/VehicleForm.vue`

**Code snippet**

```vue
<label v-for="feature in vehicleFeatures" :key="feature" class="flex items-center gap-3 rounded-lg border border-gray-200 p-3">
  <input v-model="form.features_json[feature]" type="checkbox" class="rounded border-gray-300" />
  <span class="text-sm text-gray-800">{{ humanize(feature) }}</span>
</label>
```

**Purpose of usage**

- Display (UI)
- Data storage

**Notes**

- This is the main create/edit UI for feature selection
- The form stores feature state as keyed booleans under `form.features_json`

---

### 1.6 resources/js/Components/Vehicles/VehicleForm.vue

**File path**

`resources/js/Components/Vehicles/VehicleForm.vue`

**Code snippet**

```vue
features_json: buildFeatureState(),

const enabledFeaturesCount = computed(() => Object.values(form.features_json).filter(Boolean).length);
```

**Purpose of usage**

- Display (UI)
- Data storage

**Notes**

- Initializes frontend feature state
- Computes the count of enabled checkboxes for the UI summary

---

### 1.7 resources/js/Components/Vehicles/VehicleForm.vue

**File path**

`resources/js/Components/Vehicles/VehicleForm.vue`

**Code snippet**

```vue
function buildFeatureState() {
  const rawFeatures = props.initialVehicle?.features_json;
  const enabledFeatures = Array.isArray(rawFeatures)
    ? rawFeatures
    : Object.entries(rawFeatures || {})
        .filter(([, enabled]) => Boolean(enabled))
        .map(([feature]) => feature);

  return props.vehicleFeatures.reduce((state, feature) => {
    state[feature] = enabledFeatures.includes(feature);
    return state;
  }, {});
}
```

**Purpose of usage**

- Display (UI)
- Data storage

**Notes**

- Hydrates edit forms from incoming `initialVehicle.features_json`
- Supports both array and keyed-object representations
- No `JSON.parse(...)` is used here because the frontend expects an already-parsed object/array from the model serialization

---

### 1.8 resources/js/Components/Vehicles/VehicleForm.vue

**File path**

`resources/js/Components/Vehicles/VehicleForm.vue`

**Code snippet**

```vue
form.transform((data) => ({
  ...data,
  features_json: JSON.stringify(data.features_json ?? {}),
  dynamic_attributes: JSON.stringify(serializedDynamicAttributes),
})).post(props.submitRoute, {
  forceFormData: true,
});
```

**Purpose of usage**

- Data storage

**Notes**

- Frontend serializes `features_json` into a JSON string before multipart form submission
- This is why the request classes need `json_decode(...)`
- This is the only frontend JSON conversion related to `features_json` that was found

## 2. Derived Feature Usage Depending on features_json

These usages do not reference `features_json` directly in code, but they depend on it operationally.

### 2.1 app/Models/Vehicle.php

**File path**

`app/Models/Vehicle.php`

**Code snippet**

```php
protected $appends = ['primary_image_url', 'price_with_discount', 'route_key', 'feature_labels'];

public function getFeatureLabelsAttribute(): array
{
    $featureLabels = config('vehicle_features.labels', []);

    return $this->normalizedFeatureKeys()
        ->map(fn (string $feature) => $featureLabels[$feature] ?? Str::of($feature)->replace('_', ' ')->title()->toString())
        ->values()
        ->all();
}
```

**Purpose of usage**

- Display (UI)
- API response

**Notes**

- `feature_labels` is an appended attribute on every serialized `Vehicle`
- This is the main bridge from stored `features_json` to human-readable UI output
- Any API or Inertia response returning a `Vehicle` model will also expose `feature_labels`

---

### 2.2 config/vehicle_features.php

**File path**

`config/vehicle_features.php`

**Code snippet**

```php
return [
    'labels' => [
        'cd_player' => 'CD Player',
        'sunroof' => 'Sunroof',
        'abs' => 'ABS',
        'airbags' => 'Airbags',
        'navigation' => 'Navigation',
        // ...
    ],
];
```

**Purpose of usage**

- Display (UI)

**Notes**

- Provides the human-readable label map used by `getFeatureLabelsAttribute()`
- Not a direct `features_json` usage, but required for feature display

---

### 2.3 app/Http/Controllers/Admin/VehicleController.php

**File path**

`app/Http/Controllers/Admin/VehicleController.php`

**Code snippet**

```php
return Inertia::render('Admin/Vehicles/Create', [
    // ...
    'vehicleFeatures' => Vehicle::FEATURE_OPTIONS,
]);

return Inertia::render('Vehicles/Edit', [
    'vehicle' => $vehicle->load(['images', 'attributeValues.value', 'attributeValues.attribute.values']),
    // ...
    'vehicleFeatures' => Vehicle::FEATURE_OPTIONS,
]);
```

**Purpose of usage**

- Display (UI)
- API response

**Notes**

- The controller does not inspect `features_json`
- It provides the canonical feature key list used by the shared form UI
- The `vehicle` prop sent to edit/show responses will also carry `features_json` and `feature_labels` via model serialization

---

### 2.4 app/Http/Controllers/Seller/VehicleController.php

**File path**

`app/Http/Controllers/Seller/VehicleController.php`

**Code snippet**

```php
return Inertia::render('Seller/Vehicles/Create', [
    // ...
    'vehicleFeatures' => Vehicle::FEATURE_OPTIONS,
]);

return Inertia::render('Seller/Vehicles/Edit', [
    'vehicle' => $vehicle->load(['images', 'attributeValues.value', 'attributeValues.attribute.values']),
    // ...
    'vehicleFeatures' => Vehicle::FEATURE_OPTIONS,
]);

return Inertia::render('Seller/Vehicles/Show', [
    'vehicle' => $vehicle->load(['images', 'attributeValues.attribute', 'attributeValues.value']),
    'vehicleFeatures' => Vehicle::FEATURE_OPTIONS,
]);
```

**Purpose of usage**

- Display (UI)
- API response

**Notes**

- Same pattern as admin
- `vehicleFeatures` drives checkbox rendering in the shared form
- Serialized vehicle payloads expose derived feature data to seller pages

---

### 2.5 app/Http/Controllers/PublicController.php

**File path**

`app/Http/Controllers/PublicController.php`

**Code snippet**

```php
$vehicles = $query->with(['images', 'attributeValues.attribute', 'attributeValues.value'])
    ->distinct('vehicles.id')
    ->orderBy('vehicles.created_at', 'desc')
    ->paginate(12)
    ->appends($request->query());

return Inertia::render('Public/Home', [
    'vehicles' => $vehicles,
    // ...
]);

$vehicle->load(['images', 'attributeValues.attribute', 'attributeValues.value']);

return Inertia::render('Public/VehicleShow', [
    'vehicle' => $vehicle,
]);
```

**Purpose of usage**

- API response
- Display (UI)

**Notes**

- No direct `features_json` logic exists here
- Because `Vehicle` serializes its attributes and appended `feature_labels`, public Inertia payloads can consume feature data without controller-specific transformation

---

### 2.6 app/Http/Controllers/Api/VehicleController.php

**File path**

`app/Http/Controllers/Api/VehicleController.php`

**Code snippet**

```php
$vehicles = $query->with(['images', 'seller', 'attributeValues.attribute', 'attributeValues.value'])
    ->distinct('vehicles.id')
    ->orderBy('vehicles.created_at', 'desc')
    ->paginate(12)
    ->appends($request->query());

return response()->json($vehicles);

$vehicle->load(['images', 'seller', 'attributeValues.attribute', 'attributeValues.value']);

return response()->json($vehicle);
```

**Purpose of usage**

- API response

**Notes**

- No explicit feature transformation happens in this controller
- Raw `features_json` and derived `feature_labels` are likely both present in API JSON because the `Vehicle` model exposes them by default

---

### 2.7 resources/js/Pages/Admin/Vehicles/Create.vue

**File path**

`resources/js/Pages/Admin/Vehicles/Create.vue`

**Code snippet**

```vue
<VehicleForm
  :vehicle-features="vehicleFeatures"
  :vehicle-statuses="vehicleStatuses"
  :stock-statuses="stockStatuses"
  :submit-route="route('admin.vehicles.store')"
/>
```

**Purpose of usage**

- Display (UI)

**Notes**

- This wrapper passes the supported feature list into the shared form
- It does not read `features_json` directly

---

### 2.8 resources/js/Pages/Vehicles/Edit.vue

**File path**

`resources/js/Pages/Vehicles/Edit.vue`

**Code snippet**

```vue
<VehicleForm
  :initial-vehicle="vehicle"
  :vehicle-features="vehicleFeatures"
  :submit-route="route(formAction, { vehicle: vehicle.route_key })"
  submit-method="put"
/>
```

**Purpose of usage**

- Display (UI)
- API response

**Notes**

- Edit page consumes the serialized vehicle, which includes `features_json`
- Passes feature options into the shared form so the edit UI can hydrate checkbox state

---

### 2.9 resources/js/Pages/Seller/Vehicles/Create.vue

**File path**

`resources/js/Pages/Seller/Vehicles/Create.vue`

**Code snippet**

```vue
<VehicleForm
  :vehicle-features="vehicleFeatures"
  :vehicle-statuses="vehicleStatuses"
  :stock-statuses="stockStatuses"
  :submit-route="route('seller.vehicles.store')"
/>
```

**Purpose of usage**

- Display (UI)

**Notes**

- Same feature-option propagation pattern as admin create

---

### 2.10 resources/js/Pages/Admin/Vehicles/Show.vue

**File path**

`resources/js/Pages/Admin/Vehicles/Show.vue`

**Code snippet**

```vue
<div v-if="featureLabels.length" class="md:col-span-3">
  <label class="block text-sm font-medium text-gray-700">Features</label>
  <div class="mt-2 flex flex-wrap gap-2">
    <span v-for="feature in featureLabels" :key="feature" class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700">{{ feature }}</span>
  </div>
</div>

const featureLabels = computed(() => {
  return Array.isArray(props.vehicle?.feature_labels) ? props.vehicle.feature_labels : [];
});
```

**Purpose of usage**

- Display (UI)

**Notes**

- Uses derived `feature_labels`, not raw `features_json`
- Depends on the Vehicle accessor pipeline

---

### 2.11 resources/js/Pages/Seller/Vehicles/Show.vue

**File path**

`resources/js/Pages/Seller/Vehicles/Show.vue`

**Code snippet**

```vue
<div v-if="featureLabels.length" class="md:col-span-3">
  <label class="block text-sm font-medium text-gray-700">Features</label>
  <div class="mt-2 flex flex-wrap gap-2">
    <span v-for="feature in featureLabels" :key="feature" class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700">{{ feature }}</span>
  </div>
</div>

const featureLabels = computed(() => {
  return Array.isArray(props.vehicle?.feature_labels) ? props.vehicle.feature_labels : [];
});
```

**Purpose of usage**

- Display (UI)

**Notes**

- Same as admin show

---

### 2.12 resources/js/Pages/Public/VehicleShow.vue

**File path**

`resources/js/Pages/Public/VehicleShow.vue`

**Code snippet**

```vue
<div v-if="featureLabels.length" class="mt-5 flex flex-wrap gap-3">
  <span v-for="feature in featureLabels" :key="feature" class="rounded-full border border-slate-200 bg-slate-50 px-4 py-2 text-sm font-medium text-slate-700">
    {{ feature }}
  </span>
</div>

const featureLabels = computed(() => {
  return Array.isArray(props.vehicle?.feature_labels) ? props.vehicle.feature_labels : [];
});
```

**Purpose of usage**

- Display (UI)

**Notes**

- Public-facing vehicle detail page shows human-readable feature labels only
- No JSON parsing is done in the component

---

### 2.13 resources/js/Pages/Customer/Orders/Show.vue

**File path**

`resources/js/Pages/Customer/Orders/Show.vue`

**Code snippet**

```vue
<div v-if="featureLabels(item.vehicle).length" class="mt-3 flex flex-wrap gap-2">
  <span
    v-for="feature in featureLabels(item.vehicle)"
    :key="feature"
    class="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-700"
  >
    {{ feature }}
  </span>
</div>

function featureLabels(vehicle) {
  return Array.isArray(vehicle?.feature_labels) ? vehicle.feature_labels : [];
}
```

**Purpose of usage**

- Display (UI)

**Notes**

- Customer order detail page shows features captured on the purchased vehicle
- This is another display-only consumer of derived labels

## 3. Filter/Search Logic Findings

### Direct filtering on features_json

No direct filtering logic was found for:

- `whereJsonContains('features_json', ...)`
- `where('features_json', ...)`
- any scope or service method that queries `features_json`

### Public “Features” sidebar is not features_json

**File path**

`resources/js/Pages/Public/Home.vue`

**Code snippet**

```vue
const featureAttributes = computed(() => {
  return uniqueModelAttributes.value
    .filter((attribute) => attribute.type === 'boolean')
    .sort((left, right) => featurePriorityWeight(left.name) - featurePriorityWeight(right.name) || left.name.localeCompare(right.name));
});

if (attribute.type === 'boolean' && currentFilter.raw_value === true) {
  chips.push({
    key: `attribute:${attributeKey(attribute)}:boolean`,
    label: attribute.name,
    type: 'attribute-boolean',
    attributeId: attributeKey(attribute),
  });
}
```

**Purpose of usage**

- Filtering/search

**Notes**

- This is a feature-like filter UI, but it belongs to the dynamic attribute system
- It filters boolean `model_attributes` and sends them as `attribute_filters`
- It does not read or query `features_json`

### Backend filter service confirms no features_json filtering

**File path**

`app/Services/VehicleFilterService.php`

**Code snippet**

```php
$normalized = [
    'make_id' => $this->normalizeInteger($filters['make_id'] ?? null),
    'model_id' => $this->normalizeInteger($filters['model_id'] ?? null),
    'model_code' => $this->normalizeStringArray($filters['model_code'] ?? null),
    'search' => $this->normalizeString($filters['search'] ?? null),
    'min_price' => $this->normalizeNumeric($filters['min_price'] ?? null),
    'max_price' => $this->normalizeNumeric($filters['max_price'] ?? null),
    'min_mileage' => $this->normalizeNumeric($filters['min_mileage'] ?? null),
    'max_mileage' => $this->normalizeNumeric($filters['max_mileage'] ?? null),
    'vehicle_type' => $this->normalizeString($filters['vehicle_type'] ?? null),
    'year_from' => $this->normalizeInteger($filters['year_from'] ?? null),
    'year_to' => $this->normalizeInteger($filters['year_to'] ?? null),
    'stock_country' => $this->normalizeLowerString($filters['stock_country'] ?? null),
    'attribute_filters' => $this->normalizeAttributeFilters($filters['attribute_filters'] ?? []),
];
```

**Purpose of usage**

- Filtering/search

**Notes**

- `features_json` is absent from the normalized filter keys
- No backend search, facet, or filter path uses `features_json`

## 4. Accessors / Mutators / Scopes

### Accessors

Found:

- `Vehicle::getFeatureLabelsAttribute()`

Purpose:

- Display (UI)
- API response

Notes:

- This is the main accessor exposing human-readable feature names

### Mutators

Found:

- No dedicated `setFeaturesJsonAttribute()` mutator
- No dedicated `getFeaturesJsonAttribute()` accessor

Notes:

- The model relies on Eloquent casting instead of a custom mutator/accessor pair for `features_json`

### Scopes

Found:

- No `features_json`-specific scopes

Notes:

- There is no Eloquent scope for filtering vehicles by `features_json`
- Existing scopes/filter helpers are focused on catalog attributes and legacy columns instead

## 5. API Response Exposure

### Inertia vehicle payloads

Vehicle payloads sent through Inertia are likely to contain:

- raw `features_json`
- derived `feature_labels`

Reason:

- `features_json` is a normal visible model attribute
- `feature_labels` is included in `$appends`

Affected flows include:

- Admin edit/show
- Seller edit/show
- Public show
- Public listing item payloads

### JSON API payloads

`app/Http/Controllers/Api/VehicleController.php` returns `Vehicle` models directly via `response()->json(...)`.

That means the API likely exposes:

- `features_json`
- `feature_labels`

There is no resource class or hidden attribute override in the scanned code to suppress either field.

## 6. Blade Files

Direct scan result:

- No `features_json` matches found in Blade files
- No `feature_labels` matches found in Blade files

Conclusion:

- Feature display is currently handled in Vue/Inertia, not Blade

## 7. Tests

Direct scan result:

- No `features_json` matches found in tests
- No `feature_labels` matches found in tests

Conclusion:

- There is no obvious automated test coverage specifically around `features_json`

## 8. Decode / Parse Summary

### `json_decode(...)` found

1. `app/Http/Requests/StoreVehicleRequest.php`

```php
$decoded = json_decode($this->features_json, true);
```

Purpose:

- Decode multipart-submitted JSON string before validation

2. `app/Http/Requests/UpdateVehicleRequest.php`

```php
$decoded = json_decode($this->features_json, true);
```

Purpose:

- Decode multipart-submitted JSON string before validation

3. `app/Models/Vehicle.php`

```php
if (is_string($features)) {
    $features = json_decode($features, true);
}
```

Purpose:

- Defensive normalization before deriving feature labels

### `JSON.parse(...)` found

None for `features_json`.

### `JSON.stringify(...)` found

1. `resources/js/Components/Vehicles/VehicleForm.vue`

```vue
features_json: JSON.stringify(data.features_json ?? {}),
```

Purpose:

- Encode feature flags for multipart form submission

## 9. Final Conclusions

1. `features_json` is primarily a storage and serialization field, not a search/filter field.
2. The Vehicle model is the central integration point through casting, normalization, and the `feature_labels` accessor.
3. UI display mostly avoids raw `features_json` and instead uses derived `feature_labels`.
4. Create/edit flows use `Vehicle::FEATURE_OPTIONS` plus `form.features_json[feature]` checkboxes.
5. Request classes decode incoming JSON strings and validate them as boolean maps.
6. No Blade usage, no tests, and no dedicated scopes or filtering logic were found for `features_json`.
7. The public “Features” filter is easy to confuse with `features_json`, but it actually belongs to the dynamic attribute system and is unrelated to `features_json` storage.