From 2d0285cca2147fb703ba26d840c114afebcba76d Mon Sep 17 00:00:00 2001 From: mattcoleman Date: Sun, 30 Aug 2026 15:10:59 -0500 Subject: [PATCH] feat!: full NHTSA vPIC API coverage with typed DTOs Rewrite the package from the single VinDecode class into complete coverage of the NHTSA vPIC vehicle API. - Mattsplat\VinDecode\Vpic: one method per documented vPIC endpoint (VIN decoding, WMI, manufacturers, makes, models, vehicle types, variables, equipment plant codes, parts, Canadian specifications) - Mattsplat\VinDecode\Client: Guzzle-backed HTTP wrapper, injectable - Immutable typed DTOs for every response; Response envelope wrapper - VpicException / VpicRequestException - PHPUnit suite with fixtures captured from the live API, PHPStan level 8, Laravel Pint, GitHub Actions CI (PHP 8.1-8.4) - Package hygiene: LICENSE (MIT), CHANGELOG, CONTRIBUTING, SECURITY, docs/ guides BREAKING CHANGE: root namespace is now Mattsplat\VinDecode\ and minimum PHP is 8.1. The old VinDecode\VinDecode class is kept as a deprecated shim. Claude-Session: https://claude.ai/code/session_01P1vn4Bg2KpSFyRQcnq78Cc --- .editorconfig | 15 + .gitattributes | 15 + .github/workflows/ci.yml | 41 ++ .gitignore | 7 + CHANGELOG.md | 55 ++ CONTRIBUTING.md | 38 ++ LICENSE | 21 + README.md | 172 +++++- SECURITY.md | 14 + composer.json | 48 +- docs/README.md | 43 ++ docs/canadian-specifications.md | 40 ++ docs/getting-started.md | 118 +++++ docs/manufacturers-makes.md | 79 +++ docs/models.md | 36 ++ docs/variables-equipment-parts.md | 74 +++ docs/vehicle-types.md | 34 ++ docs/vin-decoding.md | 116 +++++ docs/wmi.md | 42 ++ example.php | 24 +- phpstan.neon | 5 + phpunit.xml.dist | 20 + pint.json | 3 + src/Client.php | 109 ++++ src/DTO/Attr.php | 101 ++++ src/DTO/CanadianSpecification.php | 70 +++ src/DTO/Make.php | 55 ++ src/DTO/Manufacturer.php | 66 +++ src/DTO/ManufacturerDetail.php | 96 ++++ src/DTO/ManufacturerVehicleType.php | 36 ++ src/DTO/Model.php | 45 ++ src/DTO/Part.php | 60 +++ src/DTO/PlantCode.php | 60 +++ src/DTO/Variable.php | 56 ++ src/DTO/VariableValue.php | 38 ++ src/DTO/VehicleType.php | 47 ++ src/DTO/VinResult.php | 229 ++++++++ src/DTO/VinVariable.php | 45 ++ src/DTO/Wmi.php | 60 +++ src/DTO/WmiManufacturer.php | 57 ++ src/Exceptions/VpicException.php | 14 + src/Exceptions/VpicRequestException.php | 30 ++ src/Legacy/VinDecode.php | 88 ++++ src/Responses/Response.php | 136 +++++ src/VinDecode.php | 64 --- src/Vpic.php | 492 ++++++++++++++++++ tests/ClientTest.php | 86 +++ tests/LegacyVinDecodeTest.php | 48 ++ tests/LookupsTest.php | 102 ++++ tests/MakesAndModelsTest.php | 88 ++++ tests/ManufacturersAndWmiTest.php | 62 +++ tests/ResponseTest.php | 65 +++ tests/TestCase.php | 74 +++ tests/VinDecodingTest.php | 100 ++++ tests/fixtures/all_makes.json | 55 ++ tests/fixtures/all_manufacturers.json | 59 +++ .../canadian_vehicle_specifications.json | 33 ++ tests/fixtures/decode_vin.json | 211 ++++++++ tests/fixtures/decode_vin_batch.json | 319 ++++++++++++ tests/fixtures/decode_vin_extended.json | 217 ++++++++ tests/fixtures/decode_vin_values.json | 163 ++++++ .../fixtures/decode_vin_values_extended.json | 163 ++++++ tests/fixtures/decode_wmi.json | 18 + tests/fixtures/equipment_plant_codes.json | 117 +++++ tests/fixtures/makes_for_manufacturer.json | 22 + .../fixtures/makes_for_manufacturer_year.json | 19 + tests/fixtures/makes_for_vehicle_type.json | 79 +++ tests/fixtures/manufacturer_details.json | 57 ++ tests/fixtures/models_for_make.json | 79 +++ tests/fixtures/models_for_make_id.json | 79 +++ tests/fixtures/models_for_make_year.json | 79 +++ tests/fixtures/parts.json | 117 +++++ tests/fixtures/vehicle_types_for_make.json | 37 ++ tests/fixtures/vehicle_types_for_make_id.json | 27 + tests/fixtures/vehicle_variable_list.json | 112 ++++ tests/fixtures/vehicle_variable_values.json | 52 ++ tests/fixtures/wmis_for_manufacturer.json | 107 ++++ 77 files changed, 5876 insertions(+), 84 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 docs/README.md create mode 100644 docs/canadian-specifications.md create mode 100644 docs/getting-started.md create mode 100644 docs/manufacturers-makes.md create mode 100644 docs/models.md create mode 100644 docs/variables-equipment-parts.md create mode 100644 docs/vehicle-types.md create mode 100644 docs/vin-decoding.md create mode 100644 docs/wmi.md create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 pint.json create mode 100644 src/Client.php create mode 100644 src/DTO/Attr.php create mode 100644 src/DTO/CanadianSpecification.php create mode 100644 src/DTO/Make.php create mode 100644 src/DTO/Manufacturer.php create mode 100644 src/DTO/ManufacturerDetail.php create mode 100644 src/DTO/ManufacturerVehicleType.php create mode 100644 src/DTO/Model.php create mode 100644 src/DTO/Part.php create mode 100644 src/DTO/PlantCode.php create mode 100644 src/DTO/Variable.php create mode 100644 src/DTO/VariableValue.php create mode 100644 src/DTO/VehicleType.php create mode 100644 src/DTO/VinResult.php create mode 100644 src/DTO/VinVariable.php create mode 100644 src/DTO/Wmi.php create mode 100644 src/DTO/WmiManufacturer.php create mode 100644 src/Exceptions/VpicException.php create mode 100644 src/Exceptions/VpicRequestException.php create mode 100644 src/Legacy/VinDecode.php create mode 100644 src/Responses/Response.php delete mode 100644 src/VinDecode.php create mode 100644 src/Vpic.php create mode 100644 tests/ClientTest.php create mode 100644 tests/LegacyVinDecodeTest.php create mode 100644 tests/LookupsTest.php create mode 100644 tests/MakesAndModelsTest.php create mode 100644 tests/ManufacturersAndWmiTest.php create mode 100644 tests/ResponseTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/VinDecodingTest.php create mode 100644 tests/fixtures/all_makes.json create mode 100644 tests/fixtures/all_manufacturers.json create mode 100644 tests/fixtures/canadian_vehicle_specifications.json create mode 100644 tests/fixtures/decode_vin.json create mode 100644 tests/fixtures/decode_vin_batch.json create mode 100644 tests/fixtures/decode_vin_extended.json create mode 100644 tests/fixtures/decode_vin_values.json create mode 100644 tests/fixtures/decode_vin_values_extended.json create mode 100644 tests/fixtures/decode_wmi.json create mode 100644 tests/fixtures/equipment_plant_codes.json create mode 100644 tests/fixtures/makes_for_manufacturer.json create mode 100644 tests/fixtures/makes_for_manufacturer_year.json create mode 100644 tests/fixtures/makes_for_vehicle_type.json create mode 100644 tests/fixtures/manufacturer_details.json create mode 100644 tests/fixtures/models_for_make.json create mode 100644 tests/fixtures/models_for_make_id.json create mode 100644 tests/fixtures/models_for_make_year.json create mode 100644 tests/fixtures/parts.json create mode 100644 tests/fixtures/vehicle_types_for_make.json create mode 100644 tests/fixtures/vehicle_types_for_make_id.json create mode 100644 tests/fixtures/vehicle_variable_list.json create mode 100644 tests/fixtures/vehicle_variable_values.json create mode 100644 tests/fixtures/wmis_for_manufacturer.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..633d837 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,neon}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d1b0572 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Keep the Composer dist tarball lean — exclude dev-only files. +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.github export-ignore +/docs export-ignore +/tests export-ignore +/example.php export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon export-ignore +/pint.json export-ignore +/CONTRIBUTING.md export-ignore +/SECURITY.md export-ignore + +*.php diff=php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..099d5fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + + name: PHP ${{ matrix.php }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring + coverage: none + + - name: Validate composer.json + run: composer validate --strict + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Check code style + run: composer lint + + - name: Static analysis + run: composer analyse + + - name: Run tests + run: composer test diff --git a/.gitignore b/.gitignore index 57872d0..68d8ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ /vendor/ +/composer.lock +/.phpunit.cache/ +/.phpunit.result.cache +/.phpstan.cache/ +/build/ +/coverage/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..85e1594 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to `mattsplat/vin-decode` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +Full rewrite: complete coverage of the NHTSA vPIC vehicle API. + +### Added +- `Mattsplat\VinDecode\Vpic` — one method per documented vPIC endpoint: + - VIN decoding: `decodeVin`, `decodeVinFlat`, `decodeVinExtended`, + `decodeVinFlatExtended`, `decodeVinBatch` + - WMI: `decodeWmi`, `wmisForManufacturer` + - Manufacturers & makes: `allManufacturers`, `manufacturerDetails`, + `makesForManufacturer`, `makesForManufacturerAndYear`, `allMakes` + - Models: `modelsForMake`, `modelsForMakeId`, `modelsForMakeYear`, + `modelsForMakeIdYear` + - Vehicle types: `makesForVehicleType`, `vehicleTypesForMake`, + `vehicleTypesForMakeId` + - Variables, equipment & parts: `vehicleVariableList`, `vehicleVariableValues`, + `equipmentPlantCodes`, `parts` + - Canadian specifications: `canadianVehicleSpecifications` +- `Mattsplat\VinDecode\Client` — Guzzle-backed HTTP wrapper; accepts a custom + `ClientInterface` and base URI. +- Immutable typed DTOs for every response type under `Mattsplat\VinDecode\DTO`. +- `Mattsplat\VinDecode\Responses\Response` — iterable / countable / array-accessible + wrapper over the `{Count, Message, SearchCriteria, Results}` envelope. +- `Mattsplat\VinDecode\Exceptions\VpicException` and `VpicRequestException`. +- Test suite (PHPUnit, mocked HTTP with fixtures captured from the live API), + PHPStan (level 8) and Laravel Pint configs, and a GitHub Actions CI workflow + (PHP 8.1–8.4). +- Package hygiene: `LICENSE` (MIT), this changelog, `CONTRIBUTING.md`, + `SECURITY.md`, `.editorconfig`, `.gitattributes`, `docs/` guides. + +### Changed +- **BREAKING:** root namespace is now `Mattsplat\VinDecode\`. +- **BREAKING:** minimum PHP version is 8.1. +- `composer.json` filled out with license, authors, keywords, dev dependencies + and Composer scripts; adds `guzzlehttp/guzzle` as a runtime dependency. + +### Deprecated +- `VinDecode\VinDecode` — kept as a thin backwards-compatible shim over `Vpic` + (`setVIN()` / `searchVIN()` behave as before). Use `Mattsplat\VinDecode\Vpic` + directly. Scheduled for removal in the next major release. + +## [0.1.0] - 2018-08-13 + +### Added +- Initial `VinDecode` class: `setVIN()` + `searchVIN()` against + `vehicles/DecodeVin`, returning a snake_cased flat array. + +[Unreleased]: https://github.com/mattsplat/vin-decoder/compare/master...HEAD diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e7d82b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +Thanks for your interest in improving `mattsplat/vin-decode`. + +## Getting set up + +```bash +git clone git@github.com:mattsplat/vin-decoder.git +cd vin-decoder +composer install +``` + +## Before you open a PR + +Run the full check suite locally — CI runs the same thing on PHP 8.1 through 8.4: + +```bash +composer lint # Pint, code style (composer format to auto-fix) +composer analyse # PHPStan static analysis +composer test # PHPUnit +``` + +## Guidelines + +- **One logical change per PR.** Keep the diff focused. +- **Tests are required** for any behavioural change. HTTP is mocked with Guzzle's + `MockHandler`; response fixtures live in `tests/fixtures/` and are captured + verbatim from real vPIC API responses. When adding an endpoint, add a fixture + and a test that asserts the DTO mapping. +- **Document new endpoints** in the relevant `docs/` page and the README method + reference table. +- **Update `CHANGELOG.md`** under `## [Unreleased]`. +- Follow the existing code style (PSR-12 via Pint). Don't hand-format. + +## Reporting bugs + +Open an issue with the VIN or request parameters used, the response you got, and +the response you expected. Do not include personal data in issues. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88cf6be --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Matt Coleman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0cf2176..1ea3be2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,170 @@ -# vin-decoder -### Install -`composer require mattsplat/vin-decode` +# vin-decode -Uses National Highway Traffic Safety Administration's API to determine vehicle information +[![CI](https://github.com/mattsplat/vin-decoder/actions/workflows/ci.yml/badge.svg)](https://github.com/mattsplat/vin-decoder/actions/workflows/ci.yml) +[![Latest Version](https://img.shields.io/packagist/v/mattsplat/vin-decode.svg)](https://packagist.org/packages/mattsplat/vin-decode) +[![License](https://img.shields.io/packagist/l/mattsplat/vin-decode.svg)](LICENSE) -#### Use +A PHP wrapper for the [National Highway Traffic Safety Administration's vPIC +API](https://vpic.nhtsa.dot.gov/api/) — decode VINs and look up vehicle makes, +models, manufacturers, WMI codes, equipment plant codes and more. +- One method per documented vPIC endpoint +- Typed value objects for every response +- Guzzle under the hood; inject your own client for testing or custom middleware +- No API key required (vPIC is free and unauthenticated) + +## Install + +```bash +composer require mattsplat/vin-decode +``` + +Requires PHP 8.1+. + +## Quick start + +```php +use Mattsplat\VinDecode\Vpic; + +$vpic = new Vpic(); + +// Decode a VIN into one flat object +$vehicle = $vpic->decodeVinFlat('5UXWX7C5*BA', 2011); + +$vehicle->make(); // "BMW" +$vehicle->model(); // "X3" +$vehicle->modelYear(); // 2011 +$vehicle->bodyClass(); // "Sport Utility Vehicle (SUV)/Multipurpose Vehicle (MPV)" +$vehicle->engineCylinders(); // 6 +$vehicle->get('PlantCity'); // "MUNICH" — any raw vPIC field + +// List every model a make has registered +foreach ($vpic->modelsForMake('Honda') as $model) { + echo $model->name, "\n"; +} ``` -$vin_decode = new VinDecode(); -$vin_decode->setVIN('4G2JB3249VB205377'); -$vin_decode->searchVIN(); +List endpoints return a `Response` that is countable, iterable and array-accessible: + +```php +$response = $vpic->allManufacturers(page: 2); + +$response->count(); // rows in this response +$response->reportedCount; // vPIC's own Count field +$response->message; // vPIC status message +$response->first(); // first DTO, or null +$response->all(); // list ``` + +Errors (transport failure, non-2xx, non-JSON body) throw +`Mattsplat\VinDecode\Exceptions\VpicRequestException`, which extends +`Mattsplat\VinDecode\Exceptions\VpicException`. + +## Method reference + + + +### VIN decoding + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `decodeVin(string $vin, ?int $modelYear = null)` | `DecodeVin` | `Response` | +| `decodeVinFlat(string $vin, ?int $modelYear = null)` | `DecodeVinValues` | `VinResult` | +| `decodeVinExtended(string $vin, ?int $modelYear = null)` | `DecodeVinExtended` | `Response` | +| `decodeVinFlatExtended(string $vin, ?int $modelYear = null)` | `DecodeVinValuesExtended` | `VinResult` | +| `decodeVinBatch(array $vins)` | `DecodeVinValuesBatch` (POST, max 50) | `Response` | + +### WMI + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `decodeWmi(string $wmi)` | `DecodeWMI` | `Wmi` | +| `wmisForManufacturer(string\|int $manufacturer, ?string $vehicleType = null)` | `GetWMIsForManufacturer` | `Response` | + +### Manufacturers & makes + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `allManufacturers(?string $manufacturerType = null, int $page = 1)` | `GetAllManufacturers` | `Response` | +| `manufacturerDetails(string\|int $manufacturer, int $page = 1)` | `GetManufacturerDetails` | `Response` | +| `makesForManufacturer(string\|int $manufacturer)` | `GetMakeForManufacturer` | `Response` | +| `makesForManufacturerAndYear(string\|int $manufacturer, int $year)` | `GetMakesForManufacturerAndYear` | `Response` | +| `allMakes()` | `GetAllMakes` | `Response` | + +### Models + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `modelsForMake(string $make)` | `GetModelsForMake` | `Response` | +| `modelsForMakeId(int $makeId)` | `GetModelsForMakeId` | `Response` | +| `modelsForMakeYear(string $make, int $year, ?string $vehicleType = null)` | `GetModelsForMakeYear` | `Response` | +| `modelsForMakeIdYear(int $makeId, int $year, ?string $vehicleType = null)` | `GetModelsForMakeIdYear` | `Response` | + +### Vehicle types + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `makesForVehicleType(string $vehicleType)` | `GetMakesForVehicleType` | `Response` | +| `vehicleTypesForMake(string $make)` | `GetVehicleTypesForMake` | `Response` | +| `vehicleTypesForMakeId(int $makeId)` | `GetVehicleTypesForMakeId` | `Response` | + +### Variables, equipment & parts + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `vehicleVariableList()` | `GetVehicleVariableList` | `Response` | +| `vehicleVariableValues(string\|int $variable)` | `GetVehicleVariableValuesList` | `Response` | +| `equipmentPlantCodes(int $year, int $equipmentType, string $reportType = 'All')` | `GetEquipmentPlantCodes` | `Response` | +| `parts(int $type, string $fromDate, string $toDate, string\|int\|null $manufacturer = null, int $page = 1)` | `GetParts` | `Response` | + +### Canadian vehicle specifications + +| Method | vPIC endpoint | Returns | +| --- | --- | --- | +| `canadianVehicleSpecifications(int $year, ?string $make = null, ?string $model = null, string $units = 'Metric')` | `GetCanadianVehicleSpecifications` | `Response` | + +## Documentation + +Per-endpoint guides with request/response examples live in [`docs/`](docs/README.md). + +## Custom HTTP client + +```php +use GuzzleHttp\Client as GuzzleClient; +use Mattsplat\VinDecode\Client; +use Mattsplat\VinDecode\Vpic; + +$vpic = new Vpic(new Client(new GuzzleClient([ + 'timeout' => 5, + // ... proxy, middleware, retry handler, etc. +]))); +``` + +## Upgrading from 0.x + +The original `VinDecode\VinDecode` class still works and is kept as a thin, +**deprecated** shim over `Vpic`: + +```php +use VinDecode\VinDecode; // still works + +$decode = new VinDecode(); +$decode->setVIN('5UXWX7C5XBL41234'); +$decode->searchVIN(); // snake_cased flat array, as before +echo $decode->make; +``` + +New code should use `Mattsplat\VinDecode\Vpic` directly. See [CHANGELOG.md](CHANGELOG.md). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). Run `composer test`, `composer lint` and +`composer analyse` before opening a PR. + +## Security + +See [SECURITY.md](SECURITY.md). + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7d2d966 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported versions + +The latest tagged release receives security fixes. + +## Reporting a vulnerability + +Please do not open a public issue for security problems. Report them privately +via GitHub's [security advisories](https://github.com/mattsplat/vin-decoder/security/advisories/new) +or by emailing matthewjohncoleman@gmail.com. + +You will get an acknowledgement within a few days, and a fix or mitigation plan +once the report is confirmed. diff --git a/composer.json b/composer.json index d538c89..15476d3 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,48 @@ { "name": "mattsplat/vin-decode", - "description": "Wrapper to get VIN information", - "require": {}, + "description": "PHP wrapper for the NHTSA vPIC vehicle API (VIN decoding, makes, models, manufacturers, WMI and more).", + "keywords": ["vin", "vin-decoder", "nhtsa", "vpic", "vehicle", "automotive", "car"], + "homepage": "https://github.com/mattsplat/vin-decoder", + "license": "MIT", + "authors": [ + { + "name": "Matt Coleman", + "email": "matthewjohncoleman@gmail.com" + } + ], + "support": { + "issues": "https://github.com/mattsplat/vin-decoder/issues", + "source": "https://github.com/mattsplat/vin-decoder" + }, + "require": { + "php": "^8.1", + "guzzlehttp/guzzle": "^7.5" + }, + "require-dev": { + "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^2.1", + "laravel/pint": "^1.16" + }, "autoload": { "psr-4": { - - "VinDecode\\": "src/" - + "Mattsplat\\VinDecode\\": "src/", + "VinDecode\\": "src/Legacy/" + } + }, + "autoload-dev": { + "psr-4": { + "Mattsplat\\VinDecode\\Tests\\": "tests/" } - } + }, + "scripts": { + "test": "phpunit", + "lint": "pint --test", + "format": "pint", + "analyse": "phpstan analyse" + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "stable", + "prefer-stable": true } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2166041 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,43 @@ +# vin-decode documentation + +A PHP wrapper for the [NHTSA vPIC API](https://vpic.nhtsa.dot.gov/api/). + +## Guides + +- [Getting started](getting-started.md) — install, construct the client, responses, pagination, errors +- [VIN decoding](vin-decoding.md) — `decodeVin`, flat / extended variants, batch, the `VinResult` object +- [Manufacturers & makes](manufacturers-makes.md) +- [Models](models.md) +- [WMI](wmi.md) — World Manufacturer Identifier lookups +- [Vehicle types](vehicle-types.md) +- [Variables, equipment & parts](variables-equipment-parts.md) +- [Canadian vehicle specifications](canadian-specifications.md) + +## Method reference + +The full method → endpoint → return-type table is in the +[project README](../README.md#method-reference). + +## DTOs + +All response objects live in `Mattsplat\VinDecode\DTO` and are immutable. List +endpoints return `Mattsplat\VinDecode\Responses\Response`, which is iterable, +countable and array-accessible over the DTO rows. + +| DTO | Produced by | +| --- | --- | +| `VinResult` | `decodeVinFlat`, `decodeVinFlatExtended`, `decodeVinBatch` | +| `VinVariable` | `decodeVin`, `decodeVinExtended` | +| `Make` | `allMakes`, `makesForManufacturer`, `makesForManufacturerAndYear`, `makesForVehicleType` | +| `Model` | `modelsForMake`, `modelsForMakeId`, `modelsForMakeYear`, `modelsForMakeIdYear` | +| `Manufacturer` | `allManufacturers` | +| `ManufacturerDetail` | `manufacturerDetails` | +| `ManufacturerVehicleType` | nested in `Manufacturer` / `ManufacturerDetail` | +| `Wmi` | `decodeWmi` | +| `WmiManufacturer` | `wmisForManufacturer` | +| `VehicleType` | `vehicleTypesForMake`, `vehicleTypesForMakeId` | +| `Variable` | `vehicleVariableList` | +| `VariableValue` | `vehicleVariableValues` | +| `PlantCode` | `equipmentPlantCodes` | +| `Part` | `parts` | +| `CanadianSpecification` | `canadianVehicleSpecifications` | diff --git a/docs/canadian-specifications.md b/docs/canadian-specifications.md new file mode 100644 index 0000000..c8954f7 --- /dev/null +++ b/docs/canadian-specifications.md @@ -0,0 +1,40 @@ +# Canadian vehicle specifications + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `canadianVehicleSpecifications($year, $make = null, $model = null, $units = 'Metric')` | `GetCanadianVehicleSpecifications` | `Response` | + +Transport Canada's database of original vehicle dimensions. `$year` is required +(1971 onwards); `$make` and `$model` narrow the result. `$units` is `Metric` +(default) or `US`. + +```php +$response = $vpic->canadianVehicleSpecifications(2011, make: 'Acura'); + +foreach ($response as $spec) { + $spec->make(); // "ACURA" + $spec->model(); // "CSX" + $spec->modelYear(); // 2011 + $spec->get('Wheelbase'); + $spec->get('OverallLength'); + $spec->toArray(); // full name => value map +} +``` + +## Response shape + +vPIC returns each row as a nested `Specs` array of `{Name, Value}` pairs. +`CanadianSpecification` flattens that into a plain map, read via `get()`: + +```php +$spec->get('CurbWeight'); // "1364" +$spec->get('Missing', 'n/a'); // default for absent/blank +``` + +## Availability + +This endpoint depends on a separate Transport Canada database that is +intermittently offline. When it is down vPIC responds with HTTP 200, `Count: 0`, +an empty `Results` array, and a SQL error string in `Message` — this package +returns an **empty `Response`**, it does not throw. Check `$response->isEmpty()` +and `$response->message`. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..7ab3be9 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,118 @@ +# Getting started + +## Install + +```bash +composer require mattsplat/vin-decode +``` + +Requires PHP 8.1+ and `guzzlehttp/guzzle` ^7.5 (pulled in automatically). + +## Construct the client + +```php +use Mattsplat\VinDecode\Vpic; + +$vpic = new Vpic(); +``` + +vPIC needs no API key. Every request is sent with `format=json` — you never pass +a format yourself. + +### Custom Guzzle client + +Inject your own `Mattsplat\VinDecode\Client` to control timeouts, proxies, retry +middleware or logging: + +```php +use GuzzleHttp\Client as GuzzleClient; +use Mattsplat\VinDecode\Client; +use Mattsplat\VinDecode\Vpic; + +$vpic = new Vpic(new Client(new GuzzleClient([ + 'timeout' => 5, +]))); +``` + +You can also point the client at a different base URI (useful for a proxy or a +recorded-response server): + +```php +new Client(new GuzzleClient(), 'https://my-vpic-proxy.example/api/'); +``` + +## Responses + +### Single-object endpoints + +`decodeVinFlat()`, `decodeVinFlatExtended()` and `decodeWmi()` return a single +DTO directly. + +### List endpoints + +Everything else returns `Mattsplat\VinDecode\Responses\Response`, which wraps the +vPIC `{Count, Message, SearchCriteria, Results}` envelope: + +```php +$response = $vpic->allMakes(); + +$response->all(); // list — the mapped rows +$response->first(); // first Make, or null when empty +$response->count(); // number of rows in THIS response +$response->reportedCount; // vPIC's own "Count" field +$response->message; // vPIC status message +$response->searchCriteria; // vPIC "SearchCriteria" string, or null +$response->raw; // the untouched decoded payload +$response->isEmpty(); + +foreach ($response as $make) { /* ... */ } // iterable +$response[0]; // array-accessible (read-only) +count($response); // Countable +``` + +## Pagination + +`allManufacturers()`, `manufacturerDetails()` and `parts()` are paginated by +vPIC. Pass `page` (1-based). `GetAllManufacturers` returns 100 rows per page; +`GetParts` up to 1000. + +```php +$page1 = $vpic->allManufacturers(page: 1); +$page2 = $vpic->allManufacturers(page: 2); +``` + +## Model year + +The VIN decoders accept an optional model year. vPIC recommends supplying it — +some vehicles can only be fully decoded with it, and it disambiguates the +30-year VIN year cycle. + +```php +$vpic->decodeVinFlat('1FTFW1E50MFA00000', 2021); +``` + +## Error handling + +Transport failures, non-2xx responses and non-JSON bodies throw +`Mattsplat\VinDecode\Exceptions\VpicRequestException`: + +```php +use Mattsplat\VinDecode\Exceptions\VpicException; +use Mattsplat\VinDecode\Exceptions\VpicRequestException; + +try { + $vpic->decodeVinFlat('...'); +} catch (VpicRequestException $e) { + $e->endpoint(); // "vehicles/DecodeVinValues/..." + $e->getPrevious(); // the underlying GuzzleException, if any +} +``` + +`VpicRequestException extends VpicException extends \RuntimeException`, so catch +`VpicException` to handle every error this package raises. + +Note that vPIC itself returns HTTP 200 with an explanatory `Message` for many +"no data" cases (bad VIN, unknown make, the Canadian dataset being offline). +Those are **not** exceptions — inspect the returned DTO / `Response`. For VIN +decodes, `VinResult::isValid()` and `VinResult::errorCode()` report vPIC's +decode-level errors. diff --git a/docs/manufacturers-makes.md b/docs/manufacturers-makes.md new file mode 100644 index 0000000..ec85a31 --- /dev/null +++ b/docs/manufacturers-makes.md @@ -0,0 +1,79 @@ +# Manufacturers & makes + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `allManufacturers($manufacturerType = null, $page = 1)` | `GetAllManufacturers` | `Response` | +| `manufacturerDetails($manufacturer, $page = 1)` | `GetManufacturerDetails` | `Response` | +| `makesForManufacturer($manufacturer)` | `GetMakeForManufacturer` | `Response` | +| `makesForManufacturerAndYear($manufacturer, $year)` | `GetMakesForManufacturerAndYear` | `Response` | +| `allMakes()` | `GetAllMakes` | `Response` | + +`$manufacturer` accepts a full name, a partial name, or a numeric manufacturer ID. + +## `allManufacturers()` + +Paginated, 100 per page. + +```php +$page = $vpic->allManufacturers(page: 2); + +foreach ($page as $mfr) { + // $mfr->id 957 + // $mfr->name "BMW OF NORTH AMERICA, LLC" + // $mfr->commonName "BMW" + // $mfr->country "UNITED STATES (USA)" + // $mfr->vehicleTypes list{ name, isPrimary } +} +``` + +Filter by manufacturer type: + +```php +$vpic->allManufacturers('Completed Vehicle Manufacturer'); +``` + +## `manufacturerDetails()` + +The full registration record. + +```php +$detail = $vpic->manufacturerDetails(987)->first(); + +$detail->id; // 987 +$detail->name; // "HONDA MOTOR CO., LTD." +$detail->commonName; +$detail->country; +$detail->address; +$detail->city; +$detail->stateProvince; +$detail->postalCode; +$detail->contactEmail; +$detail->contactPhone; +$detail->primaryProduct; +$detail->principalFirstName; +$detail->principalLastName; +$detail->manufacturerTypes; // list +$detail->vehicleTypes; // list +$detail->get('SubmittedOn'); // any raw field +``` + +## `makesForManufacturer()` / `makesForManufacturerAndYear()` + +```php +foreach ($vpic->makesForManufacturer('honda') as $make) { + // $make->id 474 + // $make->name "HONDA" + // $make->manufacturerName "HONDA MOTOR CO., LTD." +} + +$vpic->makesForManufacturerAndYear('honda', 2020); // adds $make->manufacturerId +``` + +## `allMakes()` + +Every make in the dataset (12,000+). `Make` here only has `id` and `name`. + +```php +$makes = $vpic->allMakes(); // $makes->reportedCount ~= 12351 +$makes->first()->name; +``` diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..8746dc6 --- /dev/null +++ b/docs/models.md @@ -0,0 +1,36 @@ +# Models + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `modelsForMake($make)` | `GetModelsForMake` | `Response` | +| `modelsForMakeId($makeId)` | `GetModelsForMakeId` | `Response` | +| `modelsForMakeYear($make, $year, $vehicleType = null)` | `GetModelsForMakeYear` | `Response` | +| `modelsForMakeIdYear($makeId, $year, $vehicleType = null)` | `GetModelsForMakeIdYear` | `Response` | + +Every row maps to a `Model`: + +```php +foreach ($vpic->modelsForMake('Honda') as $model) { + // $model->id 1861 + // $model->name "Accord" + // $model->makeId 474 + // $model->makeName "Honda" +} +``` + +## By make ID + +```php +$vpic->modelsForMakeId(474); +``` + +## Filtered by year and vehicle type + +`$vehicleType` is a name or partial name ("car", "truck", "mpv", ...). When +given it is appended to the URL path as vPIC expects. + +```php +$vpic->modelsForMakeYear('honda', 2015); +$vpic->modelsForMakeYear('honda', 2015, 'truck'); +$vpic->modelsForMakeIdYear(474, 2015, 'truck'); +``` diff --git a/docs/variables-equipment-parts.md b/docs/variables-equipment-parts.md new file mode 100644 index 0000000..3357f58 --- /dev/null +++ b/docs/variables-equipment-parts.md @@ -0,0 +1,74 @@ +# Variables, equipment & parts + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `vehicleVariableList()` | `GetVehicleVariableList` | `Response` | +| `vehicleVariableValues($variable)` | `GetVehicleVariableValuesList` | `Response` | +| `equipmentPlantCodes($year, $equipmentType, $reportType = 'All')` | `GetEquipmentPlantCodes` | `Response` | +| `parts($type, $fromDate, $toDate, $manufacturer = null, $page = 1)` | `GetParts` | `Response` | + +## Vehicle variables + +`vehicleVariableList()` returns every variable vPIC can decode. A variable whose +`dataType` is `lookup` has a fixed set of accepted values. + +```php +foreach ($vpic->vehicleVariableList() as $variable) { + // $variable->id 2 + // $variable->name "Battery Type" + // $variable->dataType "lookup" + // $variable->groupName "Mechanical/Battery" + // $variable->description "

Battery type field stores ...

" + // $variable->isLookup() true +} +``` + +Fetch the accepted values for a lookup variable (by name or ID): + +```php +foreach ($vpic->vehicleVariableValues('battery type') as $value) { + // $value->id 3 + // $value->name "Lithium-Ion/Li-Ion" + // $value->elementName "Battery Type" +} +``` + +## Equipment plant codes + +```php +$codes = $vpic->equipmentPlantCodes(2015, equipmentType: 1, reportType: 'New'); + +foreach ($codes as $plant) { + // $plant->dotCode "00T" + // $plant->oldDotCode + // $plant->name "Hankook Tire Manufacturing Tennessee, LP" + // $plant->address / city / stateProvince / country / postalCode + // $plant->status +} +``` + +`equipmentType`: `1` tyres, `3` brake hoses, `13` glazing, `16` retread. +`reportType`: `New`, `Updated`, `Closed` (or `All`). + +## Parts (ORG regulatory submissions) + +Manufacturer submissions of a given ORG type within a date range. Paginated, +up to 1000 rows per page. Dates are `m/d/Y` strings. + +```php +$parts = $vpic->parts(565, '1/1/2015', '5/1/2015', page: 1); + +foreach ($parts as $part) { + // $part->name "ORG10655" + // $part->type "565" + // $part->letterDate "4/30/2015" + // $part->manufacturerId 987 + // $part->manufacturerName "HONDA MOTOR CO., LTD." + // $part->modelYearFrom / modelYearTo (int|null) + // $part->url + // $part->coverLetterUrl +} + +// Optionally scope to one manufacturer (name, partial, or ID): +$vpic->parts(565, '1/1/2015', '5/1/2015', manufacturer: 'honda'); +``` diff --git a/docs/vehicle-types.md b/docs/vehicle-types.md new file mode 100644 index 0000000..7fa7c8e --- /dev/null +++ b/docs/vehicle-types.md @@ -0,0 +1,34 @@ +# Vehicle types + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `makesForVehicleType($vehicleType)` | `GetMakesForVehicleType` | `Response` | +| `vehicleTypesForMake($make)` | `GetVehicleTypesForMake` | `Response` | +| `vehicleTypesForMakeId($makeId)` | `GetVehicleTypesForMakeId` | `Response` | + +`$vehicleType` is a name or partial name: `car`, `truck`, `mpv`, `motorcycle`, +`bus`, `trailer`, ... + +## `makesForVehicleType()` + +```php +foreach ($vpic->makesForVehicleType('car') as $make) { + // $make->id 441 + // $make->name "TESLA" + // $make->vehicleTypeId 2 + // $make->vehicleTypeName "Passenger Car" +} +``` + +## `vehicleTypesForMake()` / `vehicleTypesForMakeId()` + +```php +foreach ($vpic->vehicleTypesForMake('mercedes') as $type) { + // $type->id 2 + // $type->name "Passenger Car" + // $type->makeId 449 (null when looked up by ID) + // $type->makeName "MERCEDES-BENZ" +} + +$vpic->vehicleTypesForMakeId(449); // $type->makeId / makeName are null +``` diff --git a/docs/vin-decoding.md b/docs/vin-decoding.md new file mode 100644 index 0000000..c7cc798 --- /dev/null +++ b/docs/vin-decoding.md @@ -0,0 +1,116 @@ +# VIN decoding + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `decodeVin($vin, $modelYear = null)` | `DecodeVin` | `Response` | +| `decodeVinFlat($vin, $modelYear = null)` | `DecodeVinValues` | `VinResult` | +| `decodeVinExtended($vin, $modelYear = null)` | `DecodeVinExtended` | `Response` | +| `decodeVinFlatExtended($vin, $modelYear = null)` | `DecodeVinValuesExtended` | `VinResult` | +| `decodeVinBatch($vins)` | `DecodeVinValuesBatch` | `Response` | + +Partial VINs are allowed — use `*` for unknown positions (e.g. `5UXWX7C5*BA`). +The "extended" variants add NCSA program variables used in crash data. + +## `decodeVinFlat()` — one flat object + +The most common call. Returns a `VinResult` wrapping the ~150-field flat row. + +```php +$v = $vpic->decodeVinFlat('5UXWX7C5*BA', 2011); + +$v->make(); // "BMW" +$v->model(); // "X3" +$v->modelYear(); // 2011 (int) +$v->manufacturer(); // "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA" +$v->vehicleType(); // "MULTIPURPOSE PASSENGER VEHICLE (MPV)" +$v->bodyClass(); +$v->trim(); +$v->series(); +$v->doors(); // int|null +$v->engineCylinders(); // int|null +$v->displacementLitres(); // float|null +$v->engineHorsePower(); // float|null +$v->fuelTypePrimary(); +$v->fuelTypeSecondary(); +$v->electrificationLevel(); +$v->driveType(); +$v->transmissionStyle(); +$v->plantCountry(); +$v->plantCity(); +$v->vehicleDescriptor(); + +$v->errorCode(); // "0" = clean decode; comma-separated codes otherwise +$v->errorText(); +$v->isValid(); // true when errorCode is "0" or absent +``` + +Any field vPIC returns is reachable by its raw name — blank values come back as +`null`: + +```php +$v->get('BrakeSystemType'); // null when vPIC has no data +$v->get('Seats', 'unknown'); // with a default +$v['BodyCabType']; // array access, same as get() +$v->has('BatteryType'); // false +$v->toArray(); // the full underlying row +``` + +## `decodeVin()` — variable/value rows + +Returns one `VinVariable` per decoded attribute, mirroring vPIC's default shape. +Useful when you want vPIC's `VariableId` / `ValueId` codes. + +```php +foreach ($vpic->decodeVin('5UXWX7C5*BA', 2011) as $row) { + // $row->variable "Make" + // $row->value "BMW" + // $row->variableId 26 + // $row->valueId "452" +} +``` + +## `decodeVinBatch()` — up to 50 VINs + +One request, `POST` body. Each entry is a VIN string, or a `[vin, year]` pair. + +```php +$response = $vpic->decodeVinBatch([ + ['5UXWX7C5*BA', 2011], + '5YJSA3DS*EF', +]); + +foreach ($response as $vinResult) { + echo $vinResult->get('VIN'), ' => ', $vinResult->make(), "\n"; +} +``` + +Passing more than 50 entries, or an empty array, throws `VpicException`. + +## `VinResult` reference + +| Accessor | vPIC field | Type | +| --- | --- | --- | +| `make()` | `Make` | `?string` | +| `model()` | `Model` | `?string` | +| `modelYear()` | `ModelYear` | `?int` | +| `manufacturer()` | `Manufacturer` | `?string` | +| `vehicleType()` | `VehicleType` | `?string` | +| `bodyClass()` | `BodyClass` | `?string` | +| `trim()` | `Trim` | `?string` | +| `series()` | `Series` | `?string` | +| `doors()` | `Doors` | `?int` | +| `engineCylinders()` | `EngineCylinders` | `?int` | +| `displacementLitres()` | `DisplacementL` | `?float` | +| `engineHorsePower()` | `EngineHP` | `?float` | +| `fuelTypePrimary()` | `FuelTypePrimary` | `?string` | +| `fuelTypeSecondary()` | `FuelTypeSecondary` | `?string` | +| `electrificationLevel()` | `ElectrificationLevel` | `?string` | +| `driveType()` | `DriveType` | `?string` | +| `transmissionStyle()` | `TransmissionStyle` | `?string` | +| `plantCountry()` | `PlantCountry` | `?string` | +| `plantCity()` | `PlantCity` | `?string` | +| `vin()` | `VIN` | `?string` | +| `vehicleDescriptor()` | `VehicleDescriptor` | `?string` | +| `errorCode()` | `ErrorCode` | `?string` | +| `errorText()` | `ErrorText` | `?string` | +| `get($key, $default = null)` | any | `mixed` | diff --git a/docs/wmi.md b/docs/wmi.md new file mode 100644 index 0000000..47f18f1 --- /dev/null +++ b/docs/wmi.md @@ -0,0 +1,42 @@ +# WMI (World Manufacturer Identifier) + +| Method | Endpoint | Returns | +| --- | --- | --- | +| `decodeWmi($wmi)` | `DecodeWMI` | `Wmi` | +| `wmisForManufacturer($manufacturer, $vehicleType = null)` | `GetWMIsForManufacturer` | `Response` | + +The WMI is the first 3 characters of a VIN (for low-volume manufacturers, +positions 1–3 combined with 12–14). + +## `decodeWmi()` + +Returns a single `Wmi`: + +```php +$wmi = $vpic->decodeWmi('1FD'); + +$wmi->make; // "FORD" +$wmi->manufacturerName; // "FORD MOTOR COMPANY" +$wmi->commonName; // "Ford" +$wmi->parentCompanyName; +$wmi->vehicleType; // "Incomplete Vehicle" +$wmi->url; // "http://www.ford.com/" +$wmi->createdOn; +$wmi->updatedOn; +$wmi->dateAvailableToPublic; +``` + +## `wmisForManufacturer()` + +Every WMI registered to a manufacturer (name, partial name, or ID). Optionally +filter by vehicle type. + +```php +foreach ($vpic->wmisForManufacturer('hon', 'Motorcycle') as $entry) { + // $entry->wmi "JH2" + // $entry->manufacturerId 987 + // $entry->manufacturerName "HONDA MOTOR CO., LTD." + // $entry->country "JAPAN" + // $entry->vehicleType "Motorcycle" +} +``` diff --git a/example.php b/example.php index 4d61ddd..9dc355f 100644 --- a/example.php +++ b/example.php @@ -1,11 +1,23 @@ setVIN('4G2JB3249VB205377'); -$vin_decode->searchVIN(); +use Mattsplat\VinDecode\Vpic; +$vpic = new Vpic(); -var_dump($vin_decode); \ No newline at end of file +// Decode a VIN into a single flat object. +$vehicle = $vpic->decodeVinFlat('5UXWX7C5*BA', 2011); + +printf( + "%s %s %s (%s)\n", + $vehicle->modelYear(), + $vehicle->make(), + $vehicle->model(), + $vehicle->bodyClass(), +); + +// List every model Honda has registered. +foreach ($vpic->modelsForMake('Honda') as $model) { + echo $model->name, "\n"; +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0df6c87 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - src + tmpDir: .phpstan.cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c33e632 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + tests + + + + + src + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..ea5e72c --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "psr12" +} diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..a2f00ef --- /dev/null +++ b/src/Client.php @@ -0,0 +1,109 @@ +http = $http ?? new GuzzleClient([ + 'timeout' => 30, + 'headers' => ['Accept' => 'application/json'], + ]); + } + + /** + * Perform a GET request against a vPIC endpoint. + * + * @param array $query + * @return array + */ + public function get(string $path, array $query = []): array + { + return $this->request('GET', $path, ['query' => $this->normalize($query)]); + } + + /** + * Perform a POST request against a vPIC endpoint (used by the batch decoder). + * + * @param array $form + * @return array + */ + public function post(string $path, array $form = []): array + { + return $this->request('POST', $path, ['form_params' => $this->normalize($form)]); + } + + /** + * @param array $options + * @return array + */ + private function request(string $method, string $path, array $options): array + { + $path = ltrim($path, '/'); + $options['query'] = array_merge($options['query'] ?? [], ['format' => 'json']); + + try { + $response = $this->http->request($method, $this->baseUri.$path, $options); + } catch (GuzzleException $e) { + throw new VpicRequestException( + sprintf('Request to vPIC endpoint "%s" failed: %s', $path, $e->getMessage()), + $path, + $e, + ); + } + + $body = (string) $response->getBody(); + $decoded = json_decode($body, true); + + if (! is_array($decoded)) { + throw new VpicRequestException( + sprintf('vPIC endpoint "%s" returned a response that is not valid JSON.', $path), + $path, + ); + } + + /** @var array $decoded */ + return $decoded; + } + + /** + * Drop null values and cast booleans to the strings vPIC expects. + * + * @param array $params + * @return array + */ + private function normalize(array $params): array + { + $normalized = []; + + foreach ($params as $key => $value) { + if ($value === null) { + continue; + } + + $normalized[$key] = is_bool($value) ? ($value ? 'true' : 'false') : $value; + } + + return $normalized; + } +} diff --git a/src/DTO/Attr.php b/src/DTO/Attr.php new file mode 100644 index 0000000..8732471 --- /dev/null +++ b/src/DTO/Attr.php @@ -0,0 +1,101 @@ + $row + * @param string|list $keys + */ + public static function raw(array $row, string|array $keys): mixed + { + foreach ((array) $keys as $key) { + if (array_key_exists($key, $row) && $row[$key] !== null) { + return $row[$key]; + } + } + + return null; + } + + /** + * @param array $row + * @param string|list $keys + */ + public static function string(array $row, string|array $keys, string $default = ''): string + { + $value = self::raw($row, $keys); + + return $value === null ? $default : trim((string) $value); + } + + /** + * Like string(), but an empty/whitespace value becomes null. + * + * @param array $row + * @param string|list $keys + */ + public static function nullableString(array $row, string|array $keys): ?string + { + $value = self::string($row, $keys); + + return $value === '' ? null : $value; + } + + /** + * @param array $row + * @param string|list $keys + */ + public static function int(array $row, string|array $keys, int $default = 0): int + { + return self::nullableInt($row, $keys) ?? $default; + } + + /** + * @param array $row + * @param string|list $keys + */ + public static function nullableInt(array $row, string|array $keys): ?int + { + $value = self::raw($row, $keys); + + if ($value === null || $value === '') { + return null; + } + + return is_numeric($value) ? (int) $value : null; + } + + /** + * @param array $row + * @param string|list $keys + */ + public static function bool(array $row, string|array $keys): ?bool + { + $value = self::raw($row, $keys); + + if (is_bool($value)) { + return $value; + } + + if ($value === null || $value === '') { + return null; + } + + return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } +} diff --git a/src/DTO/CanadianSpecification.php b/src/DTO/CanadianSpecification.php new file mode 100644 index 0000000..6c1bdef --- /dev/null +++ b/src/DTO/CanadianSpecification.php @@ -0,0 +1,70 @@ + value map exposed via {@see self::get()}. + */ +final readonly class CanadianSpecification +{ + /** + * @param array $specs + */ + public function __construct( + public array $specs, + ) { + } + + /** + * @param array $row + */ + public static function fromArray(array $row): self + { + $specs = []; + + foreach ((array) ($row['Specs'] ?? []) as $pair) { + if (is_array($pair) && isset($pair['Name'])) { + $specs[(string) $pair['Name']] = trim((string) ($pair['Value'] ?? '')); + } + } + + return new self($specs); + } + + public function get(string $name, ?string $default = null): ?string + { + $value = $this->specs[$name] ?? null; + + return ($value === null || $value === '') ? $default : $value; + } + + public function make(): ?string + { + return $this->get('Make'); + } + + public function model(): ?string + { + return $this->get('Model'); + } + + public function modelYear(): ?int + { + $year = $this->get('myear') ?? $this->get('ModelYear'); + + return $year === null ? null : (int) $year; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->specs; + } +} diff --git a/src/DTO/Make.php b/src/DTO/Make.php new file mode 100644 index 0000000..3aeb88a --- /dev/null +++ b/src/DTO/Make.php @@ -0,0 +1,55 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + id: Attr::int($row, ['Make_ID', 'MakeId', 'MakeID']), + name: Attr::string($row, ['Make_Name', 'MakeName']), + manufacturerId: Attr::nullableInt($row, ['MfrId', 'Mfr_ID', 'ManufacturerId']), + manufacturerName: Attr::nullableString($row, ['Mfr_Name', 'MfrName', 'ManufacturerName']), + vehicleTypeId: Attr::nullableInt($row, 'VehicleTypeId'), + vehicleTypeName: Attr::nullableString($row, 'VehicleTypeName'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'manufacturerId' => $this->manufacturerId, + 'manufacturerName' => $this->manufacturerName, + 'vehicleTypeId' => $this->vehicleTypeId, + 'vehicleTypeName' => $this->vehicleTypeName, + ]; + } +} diff --git a/src/DTO/Manufacturer.php b/src/DTO/Manufacturer.php new file mode 100644 index 0000000..666faa6 --- /dev/null +++ b/src/DTO/Manufacturer.php @@ -0,0 +1,66 @@ + $vehicleTypes + */ + public function __construct( + public int $id, + public string $name, + public ?string $commonName, + public ?string $country, + public array $vehicleTypes, + ) { + } + + /** + * @param array $row + */ + public static function fromArray(array $row): self + { + $types = []; + $rawTypes = $row['VehicleTypes'] ?? []; + if (is_array($rawTypes)) { + foreach ($rawTypes as $type) { + if (is_array($type)) { + $types[] = ManufacturerVehicleType::fromArray($type); + } + } + } + + return new self( + id: Attr::int($row, ['Mfr_ID', 'MfrId']), + name: Attr::string($row, ['Mfr_Name', 'MfrName']), + commonName: Attr::nullableString($row, 'Mfr_CommonName'), + country: Attr::nullableString($row, 'Country'), + vehicleTypes: $types, + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'commonName' => $this->commonName, + 'country' => $this->country, + 'vehicleTypes' => array_map( + static fn (ManufacturerVehicleType $t): array => $t->toArray(), + $this->vehicleTypes, + ), + ]; + } +} diff --git a/src/DTO/ManufacturerDetail.php b/src/DTO/ManufacturerDetail.php new file mode 100644 index 0000000..33ce34d --- /dev/null +++ b/src/DTO/ManufacturerDetail.php @@ -0,0 +1,96 @@ + $manufacturerTypes + * @param list $vehicleTypes + * @param array $raw + */ + public function __construct( + public int $id, + public string $name, + public ?string $commonName, + public ?string $country, + public ?string $address, + public ?string $address2, + public ?string $city, + public ?string $stateProvince, + public ?string $postalCode, + public ?string $contactEmail, + public ?string $contactPhone, + public ?string $contactFax, + public ?string $primaryProduct, + public ?string $principalFirstName, + public ?string $principalLastName, + public ?string $dbas, + public ?string $lastUpdated, + public ?string $otherDetails, + public array $manufacturerTypes, + public array $vehicleTypes, + public array $raw, + ) { + } + + /** + * @param array $row + */ + public static function fromArray(array $row): self + { + $mfrTypes = []; + foreach ((array) ($row['ManufacturerTypes'] ?? []) as $type) { + if (is_array($type) && isset($type['Name'])) { + $mfrTypes[] = (string) $type['Name']; + } + } + + $vehicleTypes = []; + foreach ((array) ($row['VehicleTypes'] ?? []) as $type) { + if (is_array($type)) { + $vehicleTypes[] = ManufacturerVehicleType::fromArray($type); + } + } + + return new self( + id: Attr::int($row, ['Mfr_ID', 'MfrId']), + name: Attr::string($row, ['Mfr_Name', 'MfrName']), + commonName: Attr::nullableString($row, 'Mfr_CommonName'), + country: Attr::nullableString($row, 'Country'), + address: Attr::nullableString($row, 'Address'), + address2: Attr::nullableString($row, 'Address2'), + city: Attr::nullableString($row, 'City'), + stateProvince: Attr::nullableString($row, 'StateProvince'), + postalCode: Attr::nullableString($row, 'PostalCode'), + contactEmail: Attr::nullableString($row, 'ContactEmail'), + contactPhone: Attr::nullableString($row, 'ContactPhone'), + contactFax: Attr::nullableString($row, 'ContactFax'), + primaryProduct: Attr::nullableString($row, 'PrimaryProduct'), + principalFirstName: Attr::nullableString($row, 'PrincipalFirstName'), + principalLastName: Attr::nullableString($row, 'PrincipalLastName'), + dbas: Attr::nullableString($row, 'DBAs'), + lastUpdated: Attr::nullableString($row, 'LastUpdated'), + otherDetails: Attr::nullableString($row, 'OtherManufacturerDetails'), + manufacturerTypes: $mfrTypes, + vehicleTypes: $vehicleTypes, + raw: $row, + ); + } + + /** + * Read any field straight from the underlying vPIC row. + */ + public function get(string $key, mixed $default = null): mixed + { + return $this->raw[$key] ?? $default; + } +} diff --git a/src/DTO/ManufacturerVehicleType.php b/src/DTO/ManufacturerVehicleType.php new file mode 100644 index 0000000..cfd9246 --- /dev/null +++ b/src/DTO/ManufacturerVehicleType.php @@ -0,0 +1,36 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + name: Attr::string($row, 'Name'), + isPrimary: Attr::bool($row, 'IsPrimary') ?? false, + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return ['name' => $this->name, 'isPrimary' => $this->isPrimary]; + } +} diff --git a/src/DTO/Model.php b/src/DTO/Model.php new file mode 100644 index 0000000..687cd1f --- /dev/null +++ b/src/DTO/Model.php @@ -0,0 +1,45 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + id: Attr::int($row, ['Model_ID', 'ModelId', 'ModelID']), + name: Attr::string($row, ['Model_Name', 'ModelName']), + makeId: Attr::int($row, ['Make_ID', 'MakeId', 'MakeID']), + makeName: Attr::string($row, ['Make_Name', 'MakeName']), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'makeId' => $this->makeId, + 'makeName' => $this->makeName, + ]; + } +} diff --git a/src/DTO/Part.php b/src/DTO/Part.php new file mode 100644 index 0000000..9c9346f --- /dev/null +++ b/src/DTO/Part.php @@ -0,0 +1,60 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + name: Attr::string($row, 'Name'), + type: Attr::string($row, 'Type'), + letterDate: Attr::nullableString($row, 'LetterDate'), + manufacturerId: Attr::nullableInt($row, 'ManufacturerId'), + manufacturerName: Attr::nullableString($row, 'ManufacturerName'), + modelYearFrom: Attr::nullableInt($row, 'ModelYearFrom'), + modelYearTo: Attr::nullableInt($row, 'ModelYearTo'), + url: Attr::nullableString($row, 'URL'), + coverLetterUrl: Attr::nullableString($row, 'CoverLetterURL'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => $this->type, + 'letterDate' => $this->letterDate, + 'manufacturerId' => $this->manufacturerId, + 'manufacturerName' => $this->manufacturerName, + 'modelYearFrom' => $this->modelYearFrom, + 'modelYearTo' => $this->modelYearTo, + 'url' => $this->url, + 'coverLetterUrl' => $this->coverLetterUrl, + ]; + } +} diff --git a/src/DTO/PlantCode.php b/src/DTO/PlantCode.php new file mode 100644 index 0000000..02df5da --- /dev/null +++ b/src/DTO/PlantCode.php @@ -0,0 +1,60 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + dotCode: Attr::string($row, 'DOTCode'), + oldDotCode: Attr::nullableString($row, 'OldDotCode'), + name: Attr::string($row, 'Name'), + address: Attr::nullableString($row, 'Address'), + city: Attr::nullableString($row, 'City'), + stateProvince: Attr::nullableString($row, 'StateProvince'), + country: Attr::nullableString($row, 'Country'), + postalCode: Attr::nullableString($row, 'PostalCode'), + status: Attr::nullableString($row, 'Status'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'dotCode' => $this->dotCode, + 'oldDotCode' => $this->oldDotCode, + 'name' => $this->name, + 'address' => $this->address, + 'city' => $this->city, + 'stateProvince' => $this->stateProvince, + 'country' => $this->country, + 'postalCode' => $this->postalCode, + 'status' => $this->status, + ]; + } +} diff --git a/src/DTO/Variable.php b/src/DTO/Variable.php new file mode 100644 index 0000000..7f75c40 --- /dev/null +++ b/src/DTO/Variable.php @@ -0,0 +1,56 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + id: Attr::int($row, 'ID'), + name: Attr::string($row, 'Name'), + dataType: Attr::string($row, 'DataType'), + description: Attr::nullableString($row, 'Description'), + groupName: Attr::nullableString($row, 'GroupName'), + ); + } + + public function isLookup(): bool + { + return strtolower($this->dataType) === 'lookup'; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'dataType' => $this->dataType, + 'description' => $this->description, + 'groupName' => $this->groupName, + ]; + } +} diff --git a/src/DTO/VariableValue.php b/src/DTO/VariableValue.php new file mode 100644 index 0000000..82e3238 --- /dev/null +++ b/src/DTO/VariableValue.php @@ -0,0 +1,38 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + id: Attr::int($row, 'Id'), + name: Attr::string($row, 'Name'), + elementName: Attr::string($row, 'ElementName'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return ['id' => $this->id, 'name' => $this->name, 'elementName' => $this->elementName]; + } +} diff --git a/src/DTO/VehicleType.php b/src/DTO/VehicleType.php new file mode 100644 index 0000000..3ff8eb3 --- /dev/null +++ b/src/DTO/VehicleType.php @@ -0,0 +1,47 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + id: Attr::int($row, 'VehicleTypeId'), + name: Attr::string($row, 'VehicleTypeName'), + makeId: Attr::nullableInt($row, 'MakeId'), + makeName: Attr::nullableString($row, 'MakeName'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'makeId' => $this->makeId, + 'makeName' => $this->makeName, + ]; + } +} diff --git a/src/DTO/VinResult.php b/src/DTO/VinResult.php new file mode 100644 index 0000000..d639c9d --- /dev/null +++ b/src/DTO/VinResult.php @@ -0,0 +1,229 @@ + + */ +final readonly class VinResult implements ArrayAccess +{ + /** + * @param array $attributes + */ + public function __construct( + public array $attributes, + ) { + } + + /** + * @param array $row + */ + public static function fromArray(array $row): self + { + return new self($row); + } + + /** + * Read a field by its vPIC name (e.g. "BodyClass", "EngineCylinders"). + * Returns $default when the field is absent or blank. + */ + public function get(string $key, mixed $default = null): mixed + { + $value = $this->attributes[$key] ?? null; + + if ($value === null || $value === '') { + return $default; + } + + return is_string($value) ? trim($value) : $value; + } + + public function has(string $key): bool + { + return $this->get($key) !== null; + } + + public function make(): ?string + { + return $this->get('Make'); + } + + public function model(): ?string + { + return $this->get('Model'); + } + + public function modelYear(): ?int + { + $year = $this->get('ModelYear'); + + return $year === null ? null : (int) $year; + } + + public function manufacturer(): ?string + { + return $this->get('Manufacturer'); + } + + public function vehicleType(): ?string + { + return $this->get('VehicleType'); + } + + public function bodyClass(): ?string + { + return $this->get('BodyClass'); + } + + public function trim(): ?string + { + return $this->get('Trim'); + } + + public function series(): ?string + { + return $this->get('Series'); + } + + public function doors(): ?int + { + $doors = $this->get('Doors'); + + return $doors === null ? null : (int) $doors; + } + + public function engineCylinders(): ?int + { + $cylinders = $this->get('EngineCylinders'); + + return $cylinders === null ? null : (int) $cylinders; + } + + public function displacementLitres(): ?float + { + $litres = $this->get('DisplacementL'); + + return $litres === null ? null : (float) $litres; + } + + public function engineHorsePower(): ?float + { + $hp = $this->get('EngineHP'); + + return $hp === null ? null : (float) $hp; + } + + public function fuelTypePrimary(): ?string + { + return $this->get('FuelTypePrimary'); + } + + public function fuelTypeSecondary(): ?string + { + return $this->get('FuelTypeSecondary'); + } + + public function electrificationLevel(): ?string + { + return $this->get('ElectrificationLevel'); + } + + public function driveType(): ?string + { + return $this->get('DriveType'); + } + + public function transmissionStyle(): ?string + { + return $this->get('TransmissionStyle'); + } + + public function plantCountry(): ?string + { + return $this->get('PlantCountry'); + } + + public function plantCity(): ?string + { + return $this->get('PlantCity'); + } + + public function vin(): ?string + { + return $this->get('VIN'); + } + + public function vehicleDescriptor(): ?string + { + return $this->get('VehicleDescriptor'); + } + + /** + * vPIC error code(s) for this decode. "0" means a clean decode; multiple + * codes are comma-separated. + */ + public function errorCode(): ?string + { + return $this->get('ErrorCode'); + } + + public function errorText(): ?string + { + return $this->get('ErrorText'); + } + + /** + * True when vPIC reported no decode errors (ErrorCode "0"). + */ + public function isValid(): bool + { + $code = $this->errorCode(); + + return $code === null || $code === '0'; + } + + /** + * The full underlying row, unmodified. + * + * @return array + */ + public function toArray(): array + { + return $this->attributes; + } + + public function offsetExists(mixed $offset): bool + { + return $this->has((string) $offset); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->get((string) $offset); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new VpicException('VinResult is immutable.'); + } + + public function offsetUnset(mixed $offset): void + { + throw new VpicException('VinResult is immutable.'); + } +} diff --git a/src/DTO/VinVariable.php b/src/DTO/VinVariable.php new file mode 100644 index 0000000..17a9cdf --- /dev/null +++ b/src/DTO/VinVariable.php @@ -0,0 +1,45 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + variableId: Attr::int($row, 'VariableId'), + variable: Attr::string($row, 'Variable'), + value: Attr::nullableString($row, 'Value'), + valueId: Attr::nullableString($row, 'ValueId'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'variableId' => $this->variableId, + 'variable' => $this->variable, + 'value' => $this->value, + 'valueId' => $this->valueId, + ]; + } +} diff --git a/src/DTO/Wmi.php b/src/DTO/Wmi.php new file mode 100644 index 0000000..903d5c4 --- /dev/null +++ b/src/DTO/Wmi.php @@ -0,0 +1,60 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + make: Attr::nullableString($row, 'Make'), + manufacturerName: Attr::nullableString($row, 'ManufacturerName'), + commonName: Attr::nullableString($row, 'CommonName'), + parentCompanyName: Attr::nullableString($row, 'ParentCompanyName'), + vehicleType: Attr::nullableString($row, 'VehicleType'), + url: Attr::nullableString($row, 'URL'), + createdOn: Attr::nullableString($row, 'CreatedOn'), + updatedOn: Attr::nullableString($row, 'UpdatedOn'), + dateAvailableToPublic: Attr::nullableString($row, 'DateAvailableToPublic'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'make' => $this->make, + 'manufacturerName' => $this->manufacturerName, + 'commonName' => $this->commonName, + 'parentCompanyName' => $this->parentCompanyName, + 'vehicleType' => $this->vehicleType, + 'url' => $this->url, + 'createdOn' => $this->createdOn, + 'updatedOn' => $this->updatedOn, + 'dateAvailableToPublic' => $this->dateAvailableToPublic, + ]; + } +} diff --git a/src/DTO/WmiManufacturer.php b/src/DTO/WmiManufacturer.php new file mode 100644 index 0000000..c60f954 --- /dev/null +++ b/src/DTO/WmiManufacturer.php @@ -0,0 +1,57 @@ + $row + */ + public static function fromArray(array $row): self + { + return new self( + wmi: Attr::string($row, 'WMI'), + manufacturerId: Attr::int($row, 'Id'), + manufacturerName: Attr::string($row, 'Name'), + country: Attr::nullableString($row, 'Country'), + vehicleType: Attr::nullableString($row, 'VehicleType'), + createdOn: Attr::nullableString($row, 'CreatedOn'), + updatedOn: Attr::nullableString($row, 'UpdatedOn'), + dateAvailableToPublic: Attr::nullableString($row, 'DateAvailableToPublic'), + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'wmi' => $this->wmi, + 'manufacturerId' => $this->manufacturerId, + 'manufacturerName' => $this->manufacturerName, + 'country' => $this->country, + 'vehicleType' => $this->vehicleType, + 'createdOn' => $this->createdOn, + 'updatedOn' => $this->updatedOn, + 'dateAvailableToPublic' => $this->dateAvailableToPublic, + ]; + } +} diff --git a/src/Exceptions/VpicException.php b/src/Exceptions/VpicException.php new file mode 100644 index 0000000..8a66868 --- /dev/null +++ b/src/Exceptions/VpicException.php @@ -0,0 +1,14 @@ +endpoint; + } +} diff --git a/src/Legacy/VinDecode.php b/src/Legacy/VinDecode.php new file mode 100644 index 0000000..2c0c3b3 --- /dev/null +++ b/src/Legacy/VinDecode.php @@ -0,0 +1,88 @@ + */ + public array $results = []; + + public ?string $make = null; + + public ?string $model = null; + + public ?string $year = null; + + private Vpic $vpic; + + public function __construct(?Vpic $vpic = null) + { + $this->vpic = $vpic ?? new Vpic(); + } + + /** + * @return bool false when the VIN is not 17 characters + */ + public function setVIN(string $vin) + { + if (strlen($vin) !== 17) { + return false; + } + + $this->vin = $vin; + + return true; + } + + /** + * @param string $format retained for signature compatibility; ignored (always JSON) + * @return array + */ + public function searchVIN(string $format = 'json') + { + $decoded = $this->vpic->decodeVin((string) $this->vin); + + $results = []; + foreach ($decoded as $variable) { + if ($variable->value !== null && $variable->value !== '') { + $results[$this->toSnakeCase($variable->variable)] = $variable->value; + } + } + + $this->setResults($results); + + return $this->results = $results; + } + + /** + * @param string $string + * @return string + */ + public function toSnakeCase($string) + { + return str_replace(' ', '_', strtolower($string)); + } + + /** + * @param array $results + */ + protected function setResults($results): void + { + $this->make = $results['make'] ?? null; + $this->model = $results['model'] ?? null; + $this->year = $results['model_year'] ?? null; + } +} diff --git a/src/Responses/Response.php b/src/Responses/Response.php new file mode 100644 index 0000000..0f0d38d --- /dev/null +++ b/src/Responses/Response.php @@ -0,0 +1,136 @@ + + * @implements ArrayAccess + */ +class Response implements ArrayAccess, Countable, IteratorAggregate +{ + /** + * @param list $results + * @param array $raw + */ + public function __construct( + public readonly array $results, + public readonly int $reportedCount, + public readonly string $message, + public readonly ?string $searchCriteria, + public readonly array $raw, + ) { + } + + /** + * Build a Response from a decoded vPIC payload, mapping each result row + * through $mapper. + * + * @param array $payload + * @param callable(array): T $mapper + * @return self + */ + public static function fromPayload(array $payload, callable $mapper): self + { + $rows = $payload['Results'] ?? []; + + if (! is_array($rows)) { + $rows = []; + } + + $results = []; + foreach (array_values($rows) as $row) { + $results[] = $mapper(is_array($row) ? $row : []); + } + + return new self( + $results, + isset($payload['Count']) ? (int) $payload['Count'] : count($results), + isset($payload['Message']) ? (string) $payload['Message'] : '', + isset($payload['SearchCriteria']) ? (string) $payload['SearchCriteria'] : null, + $payload, + ); + } + + /** + * All mapped rows. + * + * @return list + */ + public function all(): array + { + return $this->results; + } + + /** + * The first mapped row, or null when the response is empty. + * + * @return T|null + */ + public function first() + { + return $this->results[0] ?? null; + } + + /** + * The number of rows actually returned in this response. + * + * Note: vPIC's own `Count` field (available via `$reportedCount`) can differ + * for paginated endpoints. + */ + public function count(): int + { + return count($this->results); + } + + public function isEmpty(): bool + { + return $this->results === []; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->results); + } + + public function offsetExists(mixed $offset): bool + { + return isset($this->results[$offset]); + } + + /** + * @return T + */ + public function offsetGet(mixed $offset): mixed + { + return $this->results[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new VpicException('vPIC responses are immutable.'); + } + + public function offsetUnset(mixed $offset): void + { + throw new VpicException('vPIC responses are immutable.'); + } +} diff --git a/src/VinDecode.php b/src/VinDecode.php deleted file mode 100644 index d1355c1..0000000 --- a/src/VinDecode.php +++ /dev/null @@ -1,64 +0,0 @@ -vin = $vin; - return true; - - } - - public function searchVIN($format = 'json') - { - $url = $this->base_url . 'vehicles/DecodeVin/'. $this->vin . '?format=' . $format; - $results = file_get_contents($url); - - - $results = json_decode($results, true)['Results']; - $temp_results=[]; - foreach($results as $result){ - if(!empty($result['Value'])){ - $key = $this->toSnakeCase( $result['Variable'] ); - $temp_results += [$key => $result['Value'] ]; - } - } - - $this->setResults($temp_results); - - return $this->results = $temp_results; - } - - public function toSnakeCase($string) - { - - $string = strtolower($string); - return str_replace(' ', '_', $string); - - } - - protected function setResults($results) - { - $this->make = $results['make']; - $this->model = $results['model']; - $this->year = $results['model_year']; - } - -} \ No newline at end of file diff --git a/src/Vpic.php b/src/Vpic.php new file mode 100644 index 0000000..0e0f749 --- /dev/null +++ b/src/Vpic.php @@ -0,0 +1,492 @@ +client = $client ?? new Client(); + } + + /** + * The underlying HTTP client. + */ + public function client(): Client + { + return $this->client; + } + + /* --------------------------------------------------------------------- + | VIN decoding + | ------------------------------------------------------------------- */ + + /** + * Decode a VIN into variable/value rows (DecodeVin). + * + * @return Response + */ + public function decodeVin(string $vin, ?int $modelYear = null): Response + { + return Response::fromPayload( + $this->client->get('vehicles/DecodeVin/'.rawurlencode($vin), ['modelyear' => $modelYear]), + VinVariable::fromArray(...), + ); + } + + /** + * Decode a VIN into a single flat object (DecodeVinValues). + */ + public function decodeVinFlat(string $vin, ?int $modelYear = null): VinResult + { + $payload = $this->client->get( + 'vehicles/DecodeVinValues/'.rawurlencode($vin), + ['modelyear' => $modelYear], + ); + + return VinResult::fromArray($this->firstRow($payload)); + } + + /** + * Decode a VIN with the extended NCSA variables (DecodeVinExtended). + * + * @return Response + */ + public function decodeVinExtended(string $vin, ?int $modelYear = null): Response + { + return Response::fromPayload( + $this->client->get('vehicles/DecodeVinExtended/'.rawurlencode($vin), ['modelyear' => $modelYear]), + VinVariable::fromArray(...), + ); + } + + /** + * Decode a VIN with the extended NCSA variables, flat (DecodeVinValuesExtended). + */ + public function decodeVinFlatExtended(string $vin, ?int $modelYear = null): VinResult + { + $payload = $this->client->get( + 'vehicles/DecodeVinValuesExtended/'.rawurlencode($vin), + ['modelyear' => $modelYear], + ); + + return VinResult::fromArray($this->firstRow($payload)); + } + + /** + * Decode up to 50 VINs in one request (DecodeVinValuesBatch). + * + * Each entry is either a VIN string, or a `[vin, modelYear]` pair. + * + * @param list $vins + * @return Response + */ + public function decodeVinBatch(array $vins): Response + { + if ($vins === []) { + throw new VpicException('decodeVinBatch() requires at least one VIN.'); + } + + if (count($vins) > self::BATCH_LIMIT) { + throw new VpicException(sprintf( + 'decodeVinBatch() accepts at most %d VINs, %d given.', + self::BATCH_LIMIT, + count($vins), + )); + } + + $entries = []; + foreach ($vins as $entry) { + if (is_array($entry)) { + [$vin, $year] = [$entry[0], $entry[1] ?? null]; + $entries[] = $year === null ? $vin : $vin.','.$year; + } else { + $entries[] = $entry; + } + } + + return Response::fromPayload( + $this->client->post('vehicles/DecodeVinValuesBatch/', ['data' => implode(';', $entries)]), + VinResult::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | WMI + | ------------------------------------------------------------------- */ + + /** + * Decode a World Manufacturer Identifier (DecodeWMI). + * + * The WMI is the first 3 characters of a VIN, or positions 1-3 + 12-14. + */ + public function decodeWmi(string $wmi): Wmi + { + $payload = $this->client->get('vehicles/DecodeWMI/'.rawurlencode($wmi)); + + return Wmi::fromArray($this->firstRow($payload)); + } + + /** + * All WMI codes registered to a manufacturer (GetWMIsForManufacturer). + * + * @param string|int $manufacturer name, partial name, or ID + * @return Response + */ + public function wmisForManufacturer(string|int $manufacturer, ?string $vehicleType = null): Response + { + return Response::fromPayload( + $this->client->get( + 'vehicles/GetWMIsForManufacturer/'.rawurlencode((string) $manufacturer), + ['vehicleType' => $vehicleType], + ), + WmiManufacturer::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | Manufacturers & makes + | ------------------------------------------------------------------- */ + + /** + * All manufacturers in the vPIC dataset, paginated 100 per page (GetAllManufacturers). + * + * @param string|null $manufacturerType e.g. "Intermediate", "Completed Vehicle Manufacturer" + * @return Response + */ + public function allManufacturers(?string $manufacturerType = null, int $page = 1): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetAllManufacturers', [ + 'ManufacturerType' => $manufacturerType, + 'page' => $page, + ]), + Manufacturer::fromArray(...), + ); + } + + /** + * Full registration details for a manufacturer (GetManufacturerDetails). + * + * @param string|int $manufacturer name, partial name, or ID + * @return Response + */ + public function manufacturerDetails(string|int $manufacturer, int $page = 1): Response + { + return Response::fromPayload( + $this->client->get( + 'vehicles/GetManufacturerDetails/'.rawurlencode((string) $manufacturer), + ['page' => $page], + ), + ManufacturerDetail::fromArray(...), + ); + } + + /** + * All makes produced by a manufacturer (GetMakeForManufacturer). + * + * @param string|int $manufacturer name, partial name, or ID + * @return Response + */ + public function makesForManufacturer(string|int $manufacturer): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetMakeForManufacturer/'.rawurlencode((string) $manufacturer)), + Make::fromArray(...), + ); + } + + /** + * Makes produced by a manufacturer in a given year (GetMakesForManufacturerAndYear). + * + * @param string|int $manufacturer name, partial name, or ID + * @return Response + */ + public function makesForManufacturerAndYear(string|int $manufacturer, int $year): Response + { + return Response::fromPayload( + $this->client->get( + 'vehicles/GetMakesForManufacturerAndYear/'.rawurlencode((string) $manufacturer), + ['year' => $year], + ), + Make::fromArray(...), + ); + } + + /** + * Every make in the vPIC dataset (GetAllMakes). + * + * @return Response + */ + public function allMakes(): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetAllMakes'), + Make::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | Models + | ------------------------------------------------------------------- */ + + /** + * Models for a make name (GetModelsForMake). + * + * @return Response + */ + public function modelsForMake(string $make): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetModelsForMake/'.rawurlencode($make)), + Model::fromArray(...), + ); + } + + /** + * Models for a make ID (GetModelsForMakeId). + * + * @return Response + */ + public function modelsForMakeId(int $makeId): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetModelsForMakeId/'.$makeId), + Model::fromArray(...), + ); + } + + /** + * Models for a make name in a given year, optionally filtered by vehicle type + * (GetModelsForMakeYear). + * + * @return Response + */ + public function modelsForMakeYear(string $make, int $year, ?string $vehicleType = null): Response + { + $path = 'vehicles/GetModelsForMakeYear/make/'.rawurlencode($make).'/modelyear/'.$year; + + if ($vehicleType !== null) { + $path .= '/vehicleType/'.rawurlencode($vehicleType); + } + + return Response::fromPayload( + $this->client->get($path), + Model::fromArray(...), + ); + } + + /** + * Models for a make ID in a given year, optionally filtered by vehicle type + * (GetModelsForMakeIdYear). + * + * @return Response + */ + public function modelsForMakeIdYear(int $makeId, int $year, ?string $vehicleType = null): Response + { + $path = 'vehicles/GetModelsForMakeIdYear/makeId/'.$makeId.'/modelyear/'.$year; + + if ($vehicleType !== null) { + $path .= '/vehicleType/'.rawurlencode($vehicleType); + } + + return Response::fromPayload( + $this->client->get($path), + Model::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | Vehicle types + | ------------------------------------------------------------------- */ + + /** + * All makes for a vehicle type name or partial name (GetMakesForVehicleType). + * + * @return Response + */ + public function makesForVehicleType(string $vehicleType): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetMakesForVehicleType/'.rawurlencode($vehicleType)), + Make::fromArray(...), + ); + } + + /** + * Vehicle types for a make name (GetVehicleTypesForMake). + * + * @return Response + */ + public function vehicleTypesForMake(string $make): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetVehicleTypesForMake/'.rawurlencode($make)), + VehicleType::fromArray(...), + ); + } + + /** + * Vehicle types for a make ID (GetVehicleTypesForMakeId). + * + * @return Response + */ + public function vehicleTypesForMakeId(int $makeId): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetVehicleTypesForMakeId/'.$makeId), + VehicleType::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | Variables, equipment & parts + | ------------------------------------------------------------------- */ + + /** + * Every vehicle variable in the vPIC dataset (GetVehicleVariableList). + * + * @return Response + */ + public function vehicleVariableList(): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetVehicleVariableList'), + Variable::fromArray(...), + ); + } + + /** + * Accepted values for a lookup variable, by name or ID (GetVehicleVariableValuesList). + * + * @return Response + */ + public function vehicleVariableValues(string|int $variable): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetVehicleVariableValuesList/'.rawurlencode((string) $variable)), + VariableValue::fromArray(...), + ); + } + + /** + * Assigned equipment plant codes for a year (GetEquipmentPlantCodes). + * + * @param int $equipmentType 1 = tyres, 3 = brake hoses, 13 = glazing, 16 = retread + * @param string $reportType "New", "Updated", or "Closed" + * @return Response + */ + public function equipmentPlantCodes(int $year, int $equipmentType, string $reportType = 'All'): Response + { + return Response::fromPayload( + $this->client->get('vehicles/GetEquipmentPlantCodes', [ + 'year' => $year, + 'equipmentType' => $equipmentType, + 'reportType' => $reportType, + ]), + PlantCode::fromArray(...), + ); + } + + /** + * ORG regulatory submissions within a date range (GetParts), 1000 per page. + * + * @param int $type ORG type, e.g. 565 or 566 + * @param string $fromDate m/d/Y + * @param string $toDate m/d/Y + * @return Response + */ + public function parts( + int $type, + string $fromDate, + string $toDate, + string|int|null $manufacturer = null, + int $page = 1, + ): Response { + return Response::fromPayload( + $this->client->get('vehicles/GetParts', [ + 'type' => $type, + 'fromDate' => $fromDate, + 'toDate' => $toDate, + 'manufacturer' => $manufacturer === null ? null : (string) $manufacturer, + 'page' => $page, + ]), + Part::fromArray(...), + ); + } + + /* --------------------------------------------------------------------- + | Canadian specifications + | ------------------------------------------------------------------- */ + + /** + * Transport Canada original vehicle dimensions (GetCanadianVehicleSpecifications). + * + * @param string $units "Metric" (default) or "US" + * @return Response + */ + public function canadianVehicleSpecifications( + int $year, + ?string $make = null, + ?string $model = null, + string $units = 'Metric', + ): Response { + return Response::fromPayload( + $this->client->get('vehicles/GetCanadianVehicleSpecifications/', [ + 'year' => $year, + 'make' => $make, + 'model' => $model, + 'units' => $units, + ]), + CanadianSpecification::fromArray(...), + ); + } + + /* ------------------------------------------------------------------- */ + + /** + * @param array $payload + * @return array + */ + private function firstRow(array $payload): array + { + $rows = $payload['Results'] ?? []; + + if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) { + return $rows[0]; + } + + return []; + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..48d1fb6 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,86 @@ +fakeVpic('all_makes'); + + $vpic->allMakes(); + + $uri = $this->lastRequest()->getUri(); + $this->assertSame('vpic.nhtsa.dot.gov', $uri->getHost()); + $this->assertStringStartsWith('/api/vehicles/GetAllMakes', $uri->getPath()); + $this->assertStringContainsString('format=json', $uri->getQuery()); + } + + public function test_it_drops_null_query_parameters(): void + { + $vpic = $this->fakeVpic('decode_vin_values'); + + $vpic->decodeVinFlat('1abc'); + + $this->assertStringNotContainsString('modelyear', $this->lastRequest()->getUri()->getQuery()); + } + + public function test_it_wraps_transport_errors(): void + { + $stack = HandlerStack::create(new MockHandler([ + new PsrResponse(500, [], 'upstream on fire'), + ])); + $client = new Client(new GuzzleClient(['handler' => $stack])); + + $this->expectException(VpicRequestException::class); + $client->get('vehicles/GetAllMakes'); + } + + public function test_it_rejects_non_json_responses(): void + { + $stack = HandlerStack::create(new MockHandler([ + new PsrResponse(200, [], 'not json'), + ])); + $client = new Client(new GuzzleClient(['handler' => $stack])); + + $this->expectException(VpicRequestException::class); + $client->get('vehicles/GetAllMakes'); + } + + public function test_request_exception_carries_the_endpoint(): void + { + $stack = HandlerStack::create(new MockHandler([new PsrResponse(503)])); + $client = new Client(new GuzzleClient(['handler' => $stack])); + + try { + $client->get('vehicles/GetAllMakes'); + $this->fail('Expected VpicRequestException'); + } catch (VpicRequestException $e) { + $this->assertSame('vehicles/GetAllMakes', $e->endpoint()); + } + } + + public function test_it_can_be_pointed_at_a_custom_base_uri(): void + { + $recorded = []; + $stack = HandlerStack::create(new MockHandler([ + new PsrResponse(200, [], '{"Results":[]}'), + ])); + $stack->push(Middleware::history($recorded)); + + $client = new Client(new GuzzleClient(['handler' => $stack]), 'https://example.test/api/'); + $client->get('vehicles/GetAllMakes'); + + $this->assertSame('example.test', $recorded[0]['request']->getUri()->getHost()); + } +} diff --git a/tests/LegacyVinDecodeTest.php b/tests/LegacyVinDecodeTest.php new file mode 100644 index 0000000..bef364e --- /dev/null +++ b/tests/LegacyVinDecodeTest.php @@ -0,0 +1,48 @@ +fakeVpic('decode_vin')); + + $this->assertFalse($decoder->setVIN('too-short')); + $this->assertTrue($decoder->setVIN('5UXWX7C5XBL000000')); + } + + public function test_search_vin_returns_a_snake_cased_flat_array_and_sets_shortcuts(): void + { + $decoder = new VinDecode($this->fakeVpic('decode_vin')); + $decoder->setVIN('5UXWX7C5XBL000000'); + + $results = $decoder->searchVIN(); + + $this->assertSame('BMW', $results['make']); + $this->assertSame('X3', $results['model']); + $this->assertSame('2011', $results['model_year']); + $this->assertArrayNotHasKey('series', $results, 'blank values are dropped'); + + $this->assertSame('BMW', $decoder->make); + $this->assertSame('X3', $decoder->model); + $this->assertSame('2011', $decoder->year); + } + + public function test_it_defaults_to_a_real_vpic_client(): void + { + $decoder = new VinDecode(); + + $this->assertInstanceOf(VinDecode::class, $decoder); + // sanity: the class still constructs its own Vpic without arguments + $this->assertTrue(class_exists(Vpic::class)); + } +} diff --git a/tests/LookupsTest.php b/tests/LookupsTest.php new file mode 100644 index 0000000..22281f7 --- /dev/null +++ b/tests/LookupsTest.php @@ -0,0 +1,102 @@ +fakeVpic('vehicle_types_for_make')->vehicleTypesForMake('mercedes'); + + $this->assertContainsOnlyInstancesOf(VehicleType::class, $response->all()); + $type = $response->first(); + $this->assertSame(2, $type->id); + $this->assertSame('Passenger Car', $type->name); + $this->assertSame(449, $type->makeId); + } + + public function test_vehicle_types_for_make_id_tolerates_missing_make_fields(): void + { + $response = $this->fakeVpic('vehicle_types_for_make_id')->vehicleTypesForMakeId(449); + + $type = $response->first(); + $this->assertGreaterThan(0, $type->id); + $this->assertNull($type->makeId); + $this->assertStringContainsString('GetVehicleTypesForMakeId/449', (string) $this->lastRequest()->getUri()); + } + + public function test_vehicle_variable_list_flags_lookups(): void + { + $response = $this->fakeVpic('vehicle_variable_list')->vehicleVariableList(); + + $this->assertContainsOnlyInstancesOf(Variable::class, $response->all()); + + $lookups = array_filter($response->all(), static fn (Variable $v): bool => $v->isLookup()); + $this->assertNotEmpty($lookups); + } + + public function test_vehicle_variable_values(): void + { + $response = $this->fakeVpic('vehicle_variable_values')->vehicleVariableValues('battery type'); + + $this->assertContainsOnlyInstancesOf(VariableValue::class, $response->all()); + $value = $response->first(); + $this->assertSame('Battery Type', $value->elementName); + $this->assertSame('Lead Acid/Lead', $value->name); + } + + public function test_equipment_plant_codes_sends_all_filters(): void + { + $response = $this->fakeVpic('equipment_plant_codes') + ->equipmentPlantCodes(2015, 1, 'New'); + + $this->assertContainsOnlyInstancesOf(PlantCode::class, $response->all()); + $this->assertNotSame('', $response->first()->dotCode); + + $query = $this->lastRequest()->getUri()->getQuery(); + $this->assertStringContainsString('year=2015', $query); + $this->assertStringContainsString('equipmentType=1', $query); + $this->assertStringContainsString('reportType=New', $query); + } + + public function test_parts(): void + { + $response = $this->fakeVpic('parts') + ->parts(565, '1/1/2015', '5/1/2015', page: 1); + + $this->assertContainsOnlyInstancesOf(Part::class, $response->all()); + $part = $response->first(); + $this->assertSame('ORG10655', $part->name); + $this->assertSame('565', $part->type); + $this->assertNull($part->modelYearFrom); + + $query = $this->lastRequest()->getUri()->getQuery(); + $this->assertStringContainsString('fromDate=1%2F1%2F2015', $query); + } + + public function test_canadian_vehicle_specifications_flattens_specs(): void + { + $response = $this->fakeVpic('canadian_vehicle_specifications') + ->canadianVehicleSpecifications(2011, 'Acura'); + + $this->assertContainsOnlyInstancesOf(CanadianSpecification::class, $response->all()); + $csx = $response->first(); + $this->assertSame('ACURA', $csx->make()); + $this->assertSame('CSX', $csx->model()); + $this->assertSame(2011, $csx->modelYear()); + $this->assertSame('2700', $csx->get('Wheelbase')); + + $query = $this->lastRequest()->getUri()->getQuery(); + $this->assertStringContainsString('make=Acura', $query); + $this->assertStringContainsString('units=Metric', $query); + } +} diff --git a/tests/MakesAndModelsTest.php b/tests/MakesAndModelsTest.php new file mode 100644 index 0000000..fd6c8d1 --- /dev/null +++ b/tests/MakesAndModelsTest.php @@ -0,0 +1,88 @@ +fakeVpic('all_makes')->allMakes(); + + $this->assertContainsOnlyInstancesOf(Make::class, $response->all()); + $first = $response->first(); + $this->assertNotNull($first); + $this->assertGreaterThan(0, $first->id); + $this->assertNotSame('', $first->name); + $this->assertSame(12351, $response->reportedCount); + } + + public function test_makes_for_manufacturer_carries_the_manufacturer_name(): void + { + $response = $this->fakeVpic('makes_for_manufacturer')->makesForManufacturer('honda'); + + $make = $response->first(); + $this->assertSame(474, $make->id); + $this->assertSame('HONDA', $make->name); + $this->assertSame('HONDA MOTOR CO., LTD.', $make->manufacturerName); + $this->assertStringContainsString('GetMakeForManufacturer/honda', (string) $this->lastRequest()->getUri()); + } + + public function test_makes_for_manufacturer_and_year_sends_the_year(): void + { + $response = $this->fakeVpic('makes_for_manufacturer_year') + ->makesForManufacturerAndYear(987, 2020); + + $this->assertSame(474, $response->first()->id); + $this->assertSame(987, $response->first()->manufacturerId); + $this->assertStringContainsString('year=2020', $this->lastRequest()->getUri()->getQuery()); + } + + public function test_makes_for_vehicle_type(): void + { + $response = $this->fakeVpic('makes_for_vehicle_type')->makesForVehicleType('car'); + + $make = $response->first(); + $this->assertSame(2, $make->vehicleTypeId); + $this->assertSame('Passenger Car', $make->vehicleTypeName); + } + + public function test_models_for_make(): void + { + $response = $this->fakeVpic('models_for_make')->modelsForMake('honda'); + + $this->assertContainsOnlyInstancesOf(Model::class, $response->all()); + $accord = $response->first(); + $this->assertSame('Accord', $accord->name); + $this->assertSame(474, $accord->makeId); + $this->assertSame('Honda', $accord->makeName); + } + + public function test_models_for_make_id_builds_the_path(): void + { + $this->fakeVpic('models_for_make_id')->modelsForMakeId(474); + + $this->assertStringContainsString('GetModelsForMakeId/474', (string) $this->lastRequest()->getUri()); + } + + public function test_models_for_make_year_with_vehicle_type_segment(): void + { + $this->fakeVpic('models_for_make_year')->modelsForMakeYear('honda', 2015, 'truck'); + + $path = $this->lastRequest()->getUri()->getPath(); + $this->assertStringContainsString('/make/honda/modelyear/2015/vehicleType/truck', $path); + } + + public function test_models_for_make_id_year_without_vehicle_type(): void + { + $this->fakeVpic('models_for_make_year')->modelsForMakeIdYear(474, 2015); + + $path = $this->lastRequest()->getUri()->getPath(); + $this->assertStringContainsString('/makeId/474/modelyear/2015', $path); + $this->assertStringNotContainsString('vehicleType', $path); + } +} diff --git a/tests/ManufacturersAndWmiTest.php b/tests/ManufacturersAndWmiTest.php new file mode 100644 index 0000000..5021e0e --- /dev/null +++ b/tests/ManufacturersAndWmiTest.php @@ -0,0 +1,62 @@ +fakeVpic('all_manufacturers')->allManufacturers(page: 2); + + $this->assertContainsOnlyInstancesOf(Manufacturer::class, $response->all()); + $mfr = $response->first(); + $this->assertGreaterThan(0, $mfr->id); + $this->assertNotSame('', $mfr->name); + $this->assertIsArray($mfr->vehicleTypes); + $this->assertStringContainsString('page=2', $this->lastRequest()->getUri()->getQuery()); + } + + public function test_manufacturer_details_maps_typed_fields_and_nested_vehicle_types(): void + { + $response = $this->fakeVpic('manufacturer_details')->manufacturerDetails(987); + + $detail = $response->first(); + $this->assertInstanceOf(ManufacturerDetail::class, $detail); + $this->assertSame(987, $detail->id); + $this->assertSame('HONDA MOTOR CO., LTD.', $detail->name); + $this->assertContains('Completed Vehicle Manufacturer', $detail->manufacturerTypes); + $this->assertSame($detail->get('Mfr_Name'), $detail->name); + + $this->assertNotSame([], $detail->vehicleTypes); + $this->assertIsBool($detail->vehicleTypes[0]->isPrimary); + } + + public function test_decode_wmi(): void + { + $wmi = $this->fakeVpic('decode_wmi')->decodeWmi('1FD'); + + $this->assertInstanceOf(Wmi::class, $wmi); + $this->assertSame('FORD', $wmi->make); + $this->assertSame('FORD MOTOR COMPANY', $wmi->manufacturerName); + $this->assertSame('Incomplete Vehicle', $wmi->vehicleType); + } + + public function test_wmis_for_manufacturer(): void + { + $response = $this->fakeVpic('wmis_for_manufacturer') + ->wmisForManufacturer('hon', 'Motorcycle'); + + $this->assertContainsOnlyInstancesOf(WmiManufacturer::class, $response->all()); + $first = $response->first(); + $this->assertNotSame('', $first->wmi); + $this->assertGreaterThan(0, $first->manufacturerId); + $this->assertStringContainsString('vehicleType=Motorcycle', $this->lastRequest()->getUri()->getQuery()); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php new file mode 100644 index 0000000..39a49bd --- /dev/null +++ b/tests/ResponseTest.php @@ -0,0 +1,65 @@ +fakeVpic('models_for_make')->modelsForMake('honda'); + + $this->assertSame(362, $response->reportedCount); + $this->assertStringContainsString('successfully', $response->message); + $this->assertSame('Make:honda', $response->searchCriteria); + $this->assertArrayHasKey('Results', $response->raw); + } + + public function test_it_is_iterable_and_countable(): void + { + $response = $this->fakeVpic('models_for_make')->modelsForMake('honda'); + + $this->assertCount(12, $response); + $this->assertSame($response->all()[0], $response[0]); + + $seen = 0; + foreach ($response as $model) { + $this->assertNotSame('', $model->name); + $seen++; + } + $this->assertSame(12, $seen); + } + + public function test_empty_payload_is_handled(): void + { + $response = $this->fakeVpic(['Count' => 0, 'Message' => 'none', 'Results' => []]) + ->modelsForMake('nope'); + + $this->assertTrue($response->isEmpty()); + $this->assertNull($response->first()); + $this->assertSame(0, $response->count()); + } + + public function test_it_is_immutable(): void + { + $response = $this->fakeVpic('models_for_make')->modelsForMake('honda'); + + $this->expectException(VpicException::class); + $response[0] = 'nope'; + } + + public function test_from_payload_defaults_count_to_row_count_when_absent(): void + { + $response = Response::fromPayload( + ['Results' => [[], []]], + static fn (array $row): array => $row, + ); + + $this->assertSame(2, $response->reportedCount); + $this->assertNull($response->searchCriteria); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..6393587 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,74 @@ + */ + protected array $recordedRequests = []; + + /** + * Build a Vpic instance whose HTTP layer replays the given canned responses. + * + * @param string|array ...$responses fixture name, raw JSON, or a payload array + */ + protected function fakeVpic(string|array ...$responses): Vpic + { + return new Vpic($this->fakeClient(...$responses)); + } + + /** + * @param string|array ...$responses + */ + protected function fakeClient(string|array ...$responses): Client + { + $queue = []; + + foreach ($responses as $response) { + $body = match (true) { + is_array($response) => json_encode($response, JSON_THROW_ON_ERROR), + str_starts_with(ltrim($response), '{') => $response, + default => $this->fixture($response), + }; + + $queue[] = new PsrResponse(200, ['Content-Type' => 'application/json'], $body); + } + + $this->recordedRequests = []; + $stack = HandlerStack::create(new MockHandler($queue)); + $stack->push(Middleware::history($this->recordedRequests)); + + return new Client(new GuzzleClient(['handler' => $stack])); + } + + protected function fixture(string $name): string + { + $path = __DIR__.'/fixtures/'.$name.'.json'; + + if (! is_file($path)) { + $this->fail("Missing fixture: {$name}.json"); + } + + return (string) file_get_contents($path); + } + + protected function lastRequest(): Request + { + $entry = end($this->recordedRequests); + $this->assertNotFalse($entry, 'No HTTP request was recorded.'); + + return $entry['request']; + } +} diff --git a/tests/VinDecodingTest.php b/tests/VinDecodingTest.php new file mode 100644 index 0000000..5a99c6f --- /dev/null +++ b/tests/VinDecodingTest.php @@ -0,0 +1,100 @@ +fakeVpic('decode_vin')->decodeVin('5UXWX7C5*BA', 2011); + + $this->assertContainsOnlyInstancesOf(VinVariable::class, $response->all()); + + $make = $this->firstWhere($response->all(), 'Make'); + $this->assertSame('BMW', $make->value); + $this->assertSame(26, $make->variableId); + + $this->assertStringContainsString('modelyear=2011', $this->lastRequest()->getUri()->getQuery()); + } + + public function test_decode_vin_flat_returns_a_single_result(): void + { + $result = $this->fakeVpic('decode_vin_values')->decodeVinFlat('5UXWX7C5*BA', 2011); + + $this->assertInstanceOf(VinResult::class, $result); + $this->assertSame('BMW', $result->make()); + $this->assertSame('X3', $result->model()); + $this->assertSame(2011, $result->modelYear()); + $this->assertSame('Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]', $result->bodyClass()); + $this->assertSame(6, $result->engineCylinders()); + } + + public function test_vin_result_exposes_unmapped_fields_and_array_access(): void + { + $result = $this->fakeVpic('decode_vin_values')->decodeVinFlat('5UXWX7C5*BA'); + + $this->assertSame('MUNICH', $result->get('PlantCity')); + $this->assertSame('MUNICH', $result['PlantCity']); + $this->assertNull($result->get('BatteryType')); + $this->assertFalse($result->has('BatteryType')); + } + + public function test_extended_decoders_hit_the_extended_endpoints(): void + { + $vpic = $this->fakeVpic('decode_vin_extended', 'decode_vin_values_extended'); + + $vpic->decodeVinExtended('1abc'); + $this->assertStringContainsString('DecodeVinExtended', (string) $this->lastRequest()->getUri()); + + $vpic->decodeVinFlatExtended('1abc'); + $this->assertStringContainsString('DecodeVinValuesExtended', (string) $this->lastRequest()->getUri()); + } + + public function test_decode_vin_batch_builds_the_data_payload(): void + { + $vpic = $this->fakeVpic('decode_vin_batch'); + + $response = $vpic->decodeVinBatch([['5UXWX7C5*BA', 2011], '5YJSA3DS*EF']); + + $this->assertContainsOnlyInstancesOf(VinResult::class, $response->all()); + $this->assertCount(2, $response); + + $body = (string) $this->lastRequest()->getBody(); + parse_str($body, $parsed); + $this->assertSame('5UXWX7C5*BA,2011;5YJSA3DS*EF', $parsed['data']); + } + + public function test_decode_vin_batch_rejects_more_than_fifty_vins(): void + { + $this->expectException(VpicException::class); + + $this->fakeVpic('decode_vin_batch')->decodeVinBatch(array_fill(0, 51, '1abc')); + } + + public function test_decode_vin_batch_rejects_an_empty_list(): void + { + $this->expectException(VpicException::class); + + $this->fakeVpic('decode_vin_batch')->decodeVinBatch([]); + } + + /** + * @param list $rows + */ + private function firstWhere(array $rows, string $variable): VinVariable + { + foreach ($rows as $row) { + if ($row->variable === $variable) { + return $row; + } + } + + $this->fail("No '{$variable}' row in response"); + } +} diff --git a/tests/fixtures/all_makes.json b/tests/fixtures/all_makes.json new file mode 100644 index 0000000..4165080 --- /dev/null +++ b/tests/fixtures/all_makes.json @@ -0,0 +1,55 @@ +{ + "Count": 12351, + "Message": "Response returned successfully", + "SearchCriteria": null, + "Results": [ + { + "Make_ID": 12858, + "Make_Name": "#1 ALPINE CUSTOMS" + }, + { + "Make_ID": 4877, + "Make_Name": "1/OFF KUSTOMS, LLC" + }, + { + "Make_ID": 11257, + "Make_Name": "102 IRONWORKS, INC." + }, + { + "Make_ID": 12255, + "Make_Name": "12832429 CANADA INC." + }, + { + "Make_ID": 13053, + "Make_Name": "137 INDUSTRIES INC." + }, + { + "Make_ID": 6387, + "Make_Name": "17 CREEK ENTERPRISES" + }, + { + "Make_ID": 12948, + "Make_Name": "1955 CUSTOM BELAIR" + }, + { + "Make_ID": 9172, + "Make_Name": "1M CUSTOM CAR TRANSPORTS, INC." + }, + { + "Make_ID": 6124, + "Make_Name": "1ST CHOICE MANUFACTURING INC" + }, + { + "Make_ID": 12972, + "Make_Name": "2 GOLDEN EAGLES" + }, + { + "Make_ID": 6488, + "Make_Name": "2-G TRAILER CO LLC" + }, + { + "Make_ID": 11399, + "Make_Name": "24/7 ONSITE CAMERAS INC" + } + ] +} diff --git a/tests/fixtures/all_manufacturers.json b/tests/fixtures/all_manufacturers.json new file mode 100644 index 0000000..7ad255a --- /dev/null +++ b/tests/fixtures/all_manufacturers.json @@ -0,0 +1,59 @@ +{ + "Count": 100, + "Message": "Response returned successfully", + "SearchCriteria": null, + "Results": [ + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Toyota", + "Mfr_ID": 1090, + "Mfr_Name": "TOYOTA MOTOR MANUFACTURING, TEXAS, INC.", + "VehicleTypes": [] + }, + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Toyota", + "Mfr_ID": 1091, + "Mfr_Name": "TOYOTA MOTOR MANUFACTURING, NORTHERN KENTUCKY, INC.", + "VehicleTypes": [] + }, + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Thomas Grinding", + "Mfr_ID": 1092, + "Mfr_Name": "THOMAS GRINDING INC.", + "VehicleTypes": [ + { + "IsPrimary": true, + "Name": "Trailer" + } + ] + }, + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Morgan Trailers", + "Mfr_ID": 1095, + "Mfr_Name": "MORGAN TRAILERS, INC", + "VehicleTypes": [ + { + "IsPrimary": true, + "Name": "Trailer" + } + ] + }, + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Altec", + "Mfr_ID": 1096, + "Mfr_Name": "ALTEC ENVIRONMENTAL PRODUCTS, LLC", + "VehicleTypes": [] + }, + { + "Country": "UNITED STATES (USA)", + "Mfr_CommonName": "Isuzu", + "Mfr_ID": 1098, + "Mfr_Name": "ISUZU MANUFACTURING SERVICES OF AMERICA, INC.", + "VehicleTypes": [] + } + ] +} diff --git a/tests/fixtures/canadian_vehicle_specifications.json b/tests/fixtures/canadian_vehicle_specifications.json new file mode 100644 index 0000000..26faeb3 --- /dev/null +++ b/tests/fixtures/canadian_vehicle_specifications.json @@ -0,0 +1,33 @@ +{ + "Count": 2, + "Message": "Results returned successfully", + "SearchCriteria": "Year: 2011; Make: Acura; Units: Metric", + "Results": [ + { + "Specs": [ + { "Name": "Make", "Value": "ACURA" }, + { "Name": "Model", "Value": "CSX" }, + { "Name": "MakeId", "Value": "475" }, + { "Name": "myear", "Value": "2011" }, + { "Name": "OverallLength", "Value": "4590" }, + { "Name": "OverallWidth", "Value": "1750" }, + { "Name": "OverallHeight", "Value": "1440" }, + { "Name": "Wheelbase", "Value": "2700" }, + { "Name": "CurbWeight", "Value": "1364" } + ] + }, + { + "Specs": [ + { "Name": "Make", "Value": "ACURA" }, + { "Name": "Model", "Value": "MDX" }, + { "Name": "MakeId", "Value": "475" }, + { "Name": "myear", "Value": "2011" }, + { "Name": "OverallLength", "Value": "4863" }, + { "Name": "OverallWidth", "Value": "1994" }, + { "Name": "OverallHeight", "Value": "1732" }, + { "Name": "Wheelbase", "Value": "2750" }, + { "Name": "CurbWeight", "Value": "2046" } + ] + } + ] +} diff --git a/tests/fixtures/decode_vin.json b/tests/fixtures/decode_vin.json new file mode 100644 index 0000000..a84feca --- /dev/null +++ b/tests/fixtures/decode_vin.json @@ -0,0 +1,211 @@ +{ + "Count": 140, + "Message": "Results returned successfully. NOTE: Any missing decoded values should be interpreted as NHTSA does not have data on the specific variable. Missing value should NOT be interpreted as an indication that a feature or technology is unavailable for a vehicle.", + "SearchCriteria": "VIN:5UXWX7C5*BA", + "Results": [ + { + "Value": "6", + "ValueId": "6", + "Variable": "Error Code", + "VariableId": 143 + }, + { + "Value": "6 - Incomplete VIN", + "ValueId": "", + "Variable": "Error Text", + "VariableId": 191 + }, + { + "Value": "5UXWX7C5*BA", + "ValueId": "", + "Variable": "Vehicle Descriptor", + "VariableId": 196 + }, + { + "Value": "BMW", + "ValueId": "452", + "Variable": "Make", + "VariableId": 26 + }, + { + "Value": "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA", + "ValueId": "968", + "Variable": "Manufacturer Name", + "VariableId": 27 + }, + { + "Value": "X3", + "ValueId": "1719", + "Variable": "Model", + "VariableId": 28 + }, + { + "Value": "2011", + "ValueId": "", + "Variable": "Model Year", + "VariableId": 29 + }, + { + "Value": "MUNICH", + "ValueId": "", + "Variable": "Plant City", + "VariableId": 31 + }, + { + "Value": "xDrive35i", + "ValueId": "", + "Variable": "Trim", + "VariableId": 38 + }, + { + "Value": "MULTIPURPOSE PASSENGER VEHICLE (MPV)", + "ValueId": "7", + "Variable": "Vehicle Type", + "VariableId": 39 + }, + { + "Value": "GERMANY", + "ValueId": "2", + "Variable": "Plant Country", + "VariableId": 75 + }, + { + "Value": "6", + "ValueId": "", + "Variable": "Engine Number of Cylinders", + "VariableId": 9 + }, + { + "Value": "2979.1682352", + "ValueId": "", + "Variable": "Displacement (CC)", + "VariableId": 11 + }, + { + "Value": "181.8", + "ValueId": "", + "Variable": "Displacement (CI)", + "VariableId": 12 + }, + { + "Value": "3.0", + "ValueId": "", + "Variable": "Displacement (L)", + "VariableId": 13 + }, + { + "Value": "Gasoline", + "ValueId": "4", + "Variable": "Fuel Type - Primary", + "VariableId": 24 + }, + { + "Value": "300", + "ValueId": "", + "Variable": "Engine Brake (hp) From", + "VariableId": 71 + }, + { + "Value": "Yes", + "ValueId": "1", + "Variable": "Pretensioner", + "VariableId": 78 + }, + { + "Value": "Manual", + "ValueId": "1", + "Variable": "Seat Belt Type", + "VariableId": 79 + }, + { + "Value": "Head Inflatable Restraint for Driver, Front Passenger, Rear Outboard Driver-side and Rear Outboard Passenger-side. Knee Inflatable Restraint for Driver. Pretensioners for Driver and Front Passenger.", + "ValueId": "", + "Variable": "Other Restraint System Info", + "VariableId": 121 + }, + { + "Value": "Direct", + "ValueId": "1", + "Variable": "Tire Pressure Monitoring System (TPMS) Type", + "VariableId": 168 + }, + { + "Value": "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]", + "ValueId": "7", + "Variable": "Body Class", + "VariableId": 5 + }, + { + "Value": "4", + "ValueId": "", + "Variable": "Doors", + "VariableId": 14 + }, + { + "Value": "AWD/All-Wheel Drive", + "ValueId": "3", + "Variable": "Drive Type", + "VariableId": 15 + }, + { + "Value": "Class 1D: 5,001 - 6,000 lb (2,268 - 2,722 kg)", + "ValueId": "13", + "Variable": "Gross Vehicle Weight Rating From", + "VariableId": 25 + }, + { + "Value": "1st Row (Driver and Passenger)", + "ValueId": "3", + "Variable": "Front Air Bag Locations", + "VariableId": 65 + }, + { + "Value": "1st Row (Driver and Passenger)", + "ValueId": "3", + "Variable": "Side Air Bag Locations", + "VariableId": 107 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Trailer Type Connection", + "VariableId": 116 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Trailer Body Type", + "VariableId": 117 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Bus Floor Configuration Type", + "VariableId": 148 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Bus Type", + "VariableId": 149 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Custom Motorcycle Type", + "VariableId": 151 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Motorcycle Suspension Type", + "VariableId": 152 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Motorcycle Chassis Type", + "VariableId": 153 + } + ] +} diff --git a/tests/fixtures/decode_vin_batch.json b/tests/fixtures/decode_vin_batch.json new file mode 100644 index 0000000..6ff555a --- /dev/null +++ b/tests/fixtures/decode_vin_batch.json @@ -0,0 +1,319 @@ +{ + "Count": 2, + "Message": "Results returned successfully. NOTE: Any missing decoded values should be interpreted as NHTSA does not have data on the specific variable. Missing value should NOT be interpreted as an indication that a feature or technology is unavailable for a vehicle.", + "SearchCriteria": "", + "Results": [ + { + "ABS": "", + "ActiveSafetySysNote": "", + "AdaptiveCruiseControl": "", + "AdaptiveDrivingBeam": "", + "AdaptiveHeadlights": "", + "AdditionalErrorText": "", + "AirBagLocCurtain": "", + "AirBagLocFront": "1st Row (Driver and Passenger)", + "AirBagLocKnee": "", + "AirBagLocSeatCushion": "", + "AirBagLocSide": "1st Row (Driver and Passenger)", + "AutoReverseSystem": "", + "AutomaticPedestrianAlertingSound": "", + "AxleConfiguration": "", + "Axles": "", + "BasePrice": "", + "BatteryA": "", + "BatteryA_to": "", + "BatteryCells": "", + "BatteryInfo": "", + "BatteryKWh": "", + "BatteryKWh_to": "", + "BatteryModules": "", + "BatteryPacks": "", + "BatteryType": "", + "BatteryV": "", + "BatteryV_to": "", + "BedLengthIN": "", + "BedType": "", + "BlindSpotIntervention": "", + "BlindSpotMon": "", + "BodyCabType": "", + "BodyClass": "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]", + "BrakeSystemDesc": "", + "BrakeSystemType": "", + "BusFloorConfigType": "Not Applicable", + "BusLength": "", + "BusType": "Not Applicable", + "CAN_AACN": "", + "CIB": "", + "CashForClunkers": "", + "ChargerLevel": "", + "ChargerPowerKW": "", + "CombinedBrakingSystem": "", + "CoolingType": "", + "CurbWeightLB": "", + "CustomMotorcycleType": "Not Applicable", + "DaytimeRunningLight": "", + "DestinationMarket": "", + "DisplacementCC": "2979.1682352", + "DisplacementCI": "181.8", + "DisplacementL": "3.0", + "Doors": "4", + "DriveType": "AWD/All-Wheel Drive", + "DriverAssist": "", + "DynamicBrakeSupport": "", + "EDR": "", + "ESC": "", + "EVDriveUnit": "", + "ElectrificationLevel": "", + "EngineConfiguration": "", + "EngineCycles": "", + "EngineCylinders": "6", + "EngineHP": "300", + "EngineHP_to": "", + "EngineKW": "", + "EngineManufacturer": "", + "EngineModel": "", + "EntertainmentSystem": "", + "ErrorCode": "6", + "ErrorText": "6 - Incomplete VIN", + "ForwardCollisionWarning": "", + "FuelInjectionType": "", + "FuelTankMaterial": "", + "FuelTankType": "", + "FuelTypePrimary": "Gasoline", + "FuelTypeSecondary": "", + "GCWR": "", + "GCWR_to": "", + "GVWR": "Class 1D: 5,001 - 6,000 lb (2,268 - 2,722 kg)", + "GVWR_to": "", + "KeylessIgnition": "", + "LaneCenteringAssistance": "", + "LaneDepartureWarning": "", + "LaneKeepSystem": "", + "LowerBeamHeadlampLightSource": "", + "Make": "BMW", + "MakeID": "452", + "Manufacturer": "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA", + "ManufacturerId": "968", + "Model": "X3", + "ModelID": "1719", + "ModelYear": "2011", + "MotorcycleChassisType": "Not Applicable", + "MotorcycleSuspensionType": "Not Applicable", + "NCSABodyType": "", + "NCSAMake": "", + "NCSAMapExcApprovedBy": "", + "NCSAMapExcApprovedOn": "", + "NCSAMappingException": "", + "NCSAModel": "", + "NCSANote": "", + "NonLandUse": "", + "Note": "", + "OtherBusInfo": "", + "OtherEngineInfo": "", + "OtherMotorcycleInfo": "", + "OtherRestraintSystemInfo": "Head Inflatable Restraint for Driver, Front Passenger, Rear Outboard Driver-side and Rear Outboard Passenger-side. Knee Inflatable Restraint for Driver. Pretensioners for Driver and Front Passenger.", + "OtherTrailerInfo": "", + "ParkAssist": "", + "PedestrianAutomaticEmergencyBraking": "", + "PlantCity": "MUNICH", + "PlantCompanyName": "", + "PlantCountry": "GERMANY", + "PlantState": "", + "PossibleValues": "", + "Pretensioner": "Yes", + "RearAutomaticEmergencyBraking": "", + "RearCrossTrafficAlert": "", + "RearVisibilitySystem": "", + "SAEAutomationLevel": "", + "SAEAutomationLevel_to": "", + "SeatBeltsAll": "Manual", + "SeatRows": "", + "Seats": "", + "SemiautomaticHeadlampBeamSwitching": "", + "Series": "", + "Series2": "", + "SteeringLocation": "", + "SuggestedVIN": "", + "TPMS": "Direct", + "TopSpeedMPH": "", + "TrackWidth": "", + "TractionControl": "", + "TrailerBodyType": "Not Applicable", + "TrailerLength": "", + "TrailerType": "Not Applicable", + "TransmissionSpeeds": "", + "TransmissionStyle": "", + "Trim": "xDrive35i", + "Trim2": "", + "Turbo": "", + "VIN": "5UXWX7C5*BA", + "ValveTrainDesign": "", + "VehicleDescriptor": "5UXWX7C5*BA", + "VehicleType": "MULTIPURPOSE PASSENGER VEHICLE (MPV)", + "WheelBaseLong": "", + "WheelBaseShort": "", + "WheelBaseType": "", + "WheelSizeFront": "", + "WheelSizeRear": "", + "WheelieMitigation": "", + "Wheels": "", + "Windows": "" + }, + { + "ABS": "", + "ActiveSafetySysNote": "", + "AdaptiveCruiseControl": "", + "AdaptiveDrivingBeam": "", + "AdaptiveHeadlights": "", + "AdditionalErrorText": "The error positions are indicated by ! in Suggested VIN. In the Possible values section, each pair of parenthesis indicate information about each error position in VIN . The Numeric value before the : indicates the position in error and the values after the : indicate the possible values that are allowed in this position Unused position(s): 6,7,8.", + "AirBagLocCurtain": "", + "AirBagLocFront": "", + "AirBagLocKnee": "", + "AirBagLocSeatCushion": "", + "AirBagLocSide": "", + "AutoReverseSystem": "", + "AutomaticPedestrianAlertingSound": "", + "AxleConfiguration": "", + "Axles": "", + "BasePrice": "", + "BatteryA": "", + "BatteryA_to": "", + "BatteryCells": "", + "BatteryInfo": "", + "BatteryKWh": "", + "BatteryKWh_to": "", + "BatteryModules": "", + "BatteryPacks": "", + "BatteryType": "", + "BatteryV": "", + "BatteryV_to": "", + "BedLengthIN": "", + "BedType": "Not Applicable", + "BlindSpotIntervention": "", + "BlindSpotMon": "", + "BodyCabType": "Not Applicable", + "BodyClass": "Hatchback/Liftback/Notchback", + "BrakeSystemDesc": "", + "BrakeSystemType": "", + "BusFloorConfigType": "Not Applicable", + "BusLength": "", + "BusType": "Not Applicable", + "CAN_AACN": "", + "CIB": "", + "CashForClunkers": "", + "ChargerLevel": "", + "ChargerPowerKW": "", + "CombinedBrakingSystem": "", + "CoolingType": "", + "CurbWeightLB": "", + "CustomMotorcycleType": "Not Applicable", + "DaytimeRunningLight": "", + "DestinationMarket": "", + "DisplacementCC": "", + "DisplacementCI": "", + "DisplacementL": "", + "Doors": "5", + "DriveType": "RWD/Rear-Wheel Drive", + "DriverAssist": "", + "DynamicBrakeSupport": "", + "EDR": "", + "ESC": "", + "EVDriveUnit": "", + "ElectrificationLevel": "", + "EngineConfiguration": "", + "EngineCycles": "", + "EngineCylinders": "", + "EngineHP": "", + "EngineHP_to": "", + "EngineKW": "", + "EngineManufacturer": "", + "EngineModel": "", + "EntertainmentSystem": "", + "ErrorCode": "5,6,14", + "ErrorText": "5 - VIN has errors in few positions; 6 - Incomplete VIN; 14 - Unable to provide information for some of the characters in the VIN, based on the manufacturer submission.", + "ForwardCollisionWarning": "", + "FuelInjectionType": "", + "FuelTankMaterial": "", + "FuelTankType": "", + "FuelTypePrimary": "", + "FuelTypeSecondary": "", + "GCWR": "", + "GCWR_to": "", + "GVWR": "Class 1: 6,000 lb or less (2,722 kg or less)", + "GVWR_to": "Class 1: 6,000 lb or less (2,722 kg or less)", + "KeylessIgnition": "", + "LaneCenteringAssistance": "", + "LaneDepartureWarning": "", + "LaneKeepSystem": "", + "LowerBeamHeadlampLightSource": "", + "Make": "TESLA", + "MakeID": "441", + "Manufacturer": "TESLA, INC.", + "ManufacturerId": "955", + "Model": "Model S", + "ModelID": "1685", + "ModelYear": "2014", + "MotorcycleChassisType": "Not Applicable", + "MotorcycleSuspensionType": "Not Applicable", + "NCSABodyType": "", + "NCSAMake": "", + "NCSAMapExcApprovedBy": "", + "NCSAMapExcApprovedOn": "", + "NCSAMappingException": "", + "NCSAModel": "", + "NCSANote": "", + "NonLandUse": "", + "Note": "", + "OtherBusInfo": "", + "OtherEngineInfo": "", + "OtherMotorcycleInfo": "", + "OtherRestraintSystemInfo": "", + "OtherTrailerInfo": "", + "ParkAssist": "", + "PedestrianAutomaticEmergencyBraking": "", + "PlantCity": "FREMONT", + "PlantCompanyName": "Tesla - Fremont, CA (FRE)", + "PlantCountry": "UNITED STATES (USA)", + "PlantState": "CALIFORNIA", + "PossibleValues": "(6:14)(7:HS)(8:12)", + "Pretensioner": "", + "RearAutomaticEmergencyBraking": "", + "RearCrossTrafficAlert": "", + "RearVisibilitySystem": "", + "SAEAutomationLevel": "", + "SAEAutomationLevel_to": "", + "SeatBeltsAll": "", + "SeatRows": "", + "Seats": "", + "SemiautomaticHeadlampBeamSwitching": "", + "Series": "", + "Series2": "", + "SteeringLocation": "Left-Hand Drive (LHD)", + "SuggestedVIN": "5YJSA!!!*EF", + "TPMS": "Direct", + "TopSpeedMPH": "", + "TrackWidth": "", + "TractionControl": "", + "TrailerBodyType": "Not Applicable", + "TrailerLength": "", + "TrailerType": "Not Applicable", + "TransmissionSpeeds": "", + "TransmissionStyle": "", + "Trim": "", + "Trim2": "", + "Turbo": "", + "VIN": "5YJSA3DS*EF", + "ValveTrainDesign": "", + "VehicleDescriptor": "5YJSA3DS*EF", + "VehicleType": "PASSENGER CAR", + "WheelBaseLong": "", + "WheelBaseShort": "", + "WheelBaseType": "", + "WheelSizeFront": "", + "WheelSizeRear": "", + "WheelieMitigation": "", + "Wheels": "", + "Windows": "" + } + ] +} diff --git a/tests/fixtures/decode_vin_extended.json b/tests/fixtures/decode_vin_extended.json new file mode 100644 index 0000000..7bc7403 --- /dev/null +++ b/tests/fixtures/decode_vin_extended.json @@ -0,0 +1,217 @@ +{ + "Count": 144, + "Message": "Results returned successfully. NOTE: Any missing decoded values should be interpreted as NHTSA does not have data on the specific variable. Missing value should NOT be interpreted as an indication that a feature or technology is unavailable for a vehicle.", + "SearchCriteria": "VIN:5UXWX7C5*BA", + "Results": [ + { + "Value": "6", + "ValueId": "6", + "Variable": "Error Code", + "VariableId": 143 + }, + { + "Value": "6 - Incomplete VIN", + "ValueId": "", + "Variable": "Error Text", + "VariableId": 191 + }, + { + "Value": "5UXWX7C5*BA", + "ValueId": "", + "Variable": "Vehicle Descriptor", + "VariableId": 196 + }, + { + "Value": "BMW", + "ValueId": "452", + "Variable": "Make", + "VariableId": 26 + }, + { + "Value": "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA", + "ValueId": "968", + "Variable": "Manufacturer Name", + "VariableId": 27 + }, + { + "Value": "X3", + "ValueId": "1719", + "Variable": "Model", + "VariableId": 28 + }, + { + "Value": "2011", + "ValueId": "", + "Variable": "Model Year", + "VariableId": 29 + }, + { + "Value": "MUNICH", + "ValueId": "", + "Variable": "Plant City", + "VariableId": 31 + }, + { + "Value": "xDrive35i", + "ValueId": "", + "Variable": "Trim", + "VariableId": 38 + }, + { + "Value": "MULTIPURPOSE PASSENGER VEHICLE (MPV)", + "ValueId": "7", + "Variable": "Vehicle Type", + "VariableId": 39 + }, + { + "Value": "GERMANY", + "ValueId": "2", + "Variable": "Plant Country", + "VariableId": 75 + }, + { + "Value": "6", + "ValueId": "", + "Variable": "Engine Number of Cylinders", + "VariableId": 9 + }, + { + "Value": "2979.1682352", + "ValueId": "", + "Variable": "Displacement (CC)", + "VariableId": 11 + }, + { + "Value": "181.8", + "ValueId": "", + "Variable": "Displacement (CI)", + "VariableId": 12 + }, + { + "Value": "3.0", + "ValueId": "", + "Variable": "Displacement (L)", + "VariableId": 13 + }, + { + "Value": "Gasoline", + "ValueId": "4", + "Variable": "Fuel Type - Primary", + "VariableId": 24 + }, + { + "Value": "300", + "ValueId": "", + "Variable": "Engine Brake (hp) From", + "VariableId": 71 + }, + { + "Value": "Yes", + "ValueId": "1", + "Variable": "Pretensioner", + "VariableId": 78 + }, + { + "Value": "Manual", + "ValueId": "1", + "Variable": "Seat Belt Type", + "VariableId": 79 + }, + { + "Value": "Head Inflatable Restraint for Driver, Front Passenger, Rear Outboard Driver-side and Rear Outboard Passenger-side. Knee Inflatable Restraint for Driver. Pretensioners for Driver and Front Passenger.", + "ValueId": "", + "Variable": "Other Restraint System Info", + "VariableId": 121 + }, + { + "Value": "Direct", + "ValueId": "1", + "Variable": "Tire Pressure Monitoring System (TPMS) Type", + "VariableId": 168 + }, + { + "Value": "Compact Utility (Utility Vehicle Categories \"Small\" and \"Midsize\")", + "ValueId": "14", + "Variable": "NCSA Body Type", + "VariableId": 96 + }, + { + "Value": "BMW", + "ValueId": "34", + "Variable": "NCSA Make", + "VariableId": 97 + }, + { + "Value": "X3", + "ValueId": "402", + "Variable": "NCSA Model", + "VariableId": 98 + }, + { + "Value": "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]", + "ValueId": "7", + "Variable": "Body Class", + "VariableId": 5 + }, + { + "Value": "4", + "ValueId": "", + "Variable": "Doors", + "VariableId": 14 + }, + { + "Value": "AWD/All-Wheel Drive", + "ValueId": "3", + "Variable": "Drive Type", + "VariableId": 15 + }, + { + "Value": "Class 1D: 5,001 - 6,000 lb (2,268 - 2,722 kg)", + "ValueId": "13", + "Variable": "Gross Vehicle Weight Rating From", + "VariableId": 25 + }, + { + "Value": "1st Row (Driver and Passenger)", + "ValueId": "3", + "Variable": "Front Air Bag Locations", + "VariableId": 65 + }, + { + "Value": "1st Row (Driver and Passenger)", + "ValueId": "3", + "Variable": "Side Air Bag Locations", + "VariableId": 107 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Trailer Type Connection", + "VariableId": 116 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Trailer Body Type", + "VariableId": 117 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Bus Floor Configuration Type", + "VariableId": 148 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Bus Type", + "VariableId": 149 + }, + { + "Value": "Not Applicable", + "ValueId": "0", + "Variable": "Custom Motorcycle Type", + "VariableId": 151 + } + ] +} diff --git a/tests/fixtures/decode_vin_values.json b/tests/fixtures/decode_vin_values.json new file mode 100644 index 0000000..6833765 --- /dev/null +++ b/tests/fixtures/decode_vin_values.json @@ -0,0 +1,163 @@ +{ + "Count": 1, + "Message": "Results returned successfully. NOTE: Any missing decoded values should be interpreted as NHTSA does not have data on the specific variable. Missing value should NOT be interpreted as an indication that a feature or technology is unavailable for a vehicle.", + "SearchCriteria": "VIN(s): 5UXWX7C5*BA", + "Results": [ + { + "ABS": "", + "ActiveSafetySysNote": "", + "AdaptiveCruiseControl": "", + "AdaptiveDrivingBeam": "", + "AdaptiveHeadlights": "", + "AdditionalErrorText": "", + "AirBagLocCurtain": "", + "AirBagLocFront": "1st Row (Driver and Passenger)", + "AirBagLocKnee": "", + "AirBagLocSeatCushion": "", + "AirBagLocSide": "1st Row (Driver and Passenger)", + "AutoReverseSystem": "", + "AutomaticPedestrianAlertingSound": "", + "AxleConfiguration": "", + "Axles": "", + "BasePrice": "", + "BatteryA": "", + "BatteryA_to": "", + "BatteryCells": "", + "BatteryInfo": "", + "BatteryKWh": "", + "BatteryKWh_to": "", + "BatteryModules": "", + "BatteryPacks": "", + "BatteryType": "", + "BatteryV": "", + "BatteryV_to": "", + "BedLengthIN": "", + "BedType": "", + "BlindSpotIntervention": "", + "BlindSpotMon": "", + "BodyCabType": "", + "BodyClass": "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]", + "BrakeSystemDesc": "", + "BrakeSystemType": "", + "BusFloorConfigType": "Not Applicable", + "BusLength": "", + "BusType": "Not Applicable", + "CAN_AACN": "", + "CIB": "", + "CashForClunkers": "", + "ChargerLevel": "", + "ChargerPowerKW": "", + "CombinedBrakingSystem": "", + "CoolingType": "", + "CurbWeightLB": "", + "CustomMotorcycleType": "Not Applicable", + "DaytimeRunningLight": "", + "DestinationMarket": "", + "DisplacementCC": "2979.1682352", + "DisplacementCI": "181.8", + "DisplacementL": "3.0", + "Doors": "4", + "DriveType": "AWD/All-Wheel Drive", + "DriverAssist": "", + "DynamicBrakeSupport": "", + "EDR": "", + "ESC": "", + "EVDriveUnit": "", + "ElectrificationLevel": "", + "EngineConfiguration": "", + "EngineCycles": "", + "EngineCylinders": "6", + "EngineHP": "300", + "EngineHP_to": "", + "EngineKW": "", + "EngineManufacturer": "", + "EngineModel": "", + "EntertainmentSystem": "", + "ErrorCode": "6", + "ErrorText": "6 - Incomplete VIN", + "ForwardCollisionWarning": "", + "FuelInjectionType": "", + "FuelTankMaterial": "", + "FuelTankType": "", + "FuelTypePrimary": "Gasoline", + "FuelTypeSecondary": "", + "GCWR": "", + "GCWR_to": "", + "GVWR": "Class 1D: 5,001 - 6,000 lb (2,268 - 2,722 kg)", + "GVWR_to": "", + "KeylessIgnition": "", + "LaneCenteringAssistance": "", + "LaneDepartureWarning": "", + "LaneKeepSystem": "", + "LowerBeamHeadlampLightSource": "", + "Make": "BMW", + "MakeID": "452", + "Manufacturer": "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA", + "ManufacturerId": "968", + "Model": "X3", + "ModelID": "1719", + "ModelYear": "2011", + "MotorcycleChassisType": "Not Applicable", + "MotorcycleSuspensionType": "Not Applicable", + "NCSABodyType": "", + "NCSAMake": "", + "NCSAMapExcApprovedBy": "", + "NCSAMapExcApprovedOn": "", + "NCSAMappingException": "", + "NCSAModel": "", + "NCSANote": "", + "NonLandUse": "", + "Note": "", + "OtherBusInfo": "", + "OtherEngineInfo": "", + "OtherMotorcycleInfo": "", + "OtherRestraintSystemInfo": "Head Inflatable Restraint for Driver, Front Passenger, Rear Outboard Driver-side and Rear Outboard Passenger-side. Knee Inflatable Restraint for Driver. Pretensioners for Driver and Front Passenger.", + "OtherTrailerInfo": "", + "ParkAssist": "", + "PedestrianAutomaticEmergencyBraking": "", + "PlantCity": "MUNICH", + "PlantCompanyName": "", + "PlantCountry": "GERMANY", + "PlantState": "", + "PossibleValues": "", + "Pretensioner": "Yes", + "RearAutomaticEmergencyBraking": "", + "RearCrossTrafficAlert": "", + "RearVisibilitySystem": "", + "SAEAutomationLevel": "", + "SAEAutomationLevel_to": "", + "SeatBeltsAll": "Manual", + "SeatRows": "", + "Seats": "", + "SemiautomaticHeadlampBeamSwitching": "", + "Series": "", + "Series2": "", + "SteeringLocation": "", + "SuggestedVIN": "", + "TPMS": "Direct", + "TopSpeedMPH": "", + "TrackWidth": "", + "TractionControl": "", + "TrailerBodyType": "Not Applicable", + "TrailerLength": "", + "TrailerType": "Not Applicable", + "TransmissionSpeeds": "", + "TransmissionStyle": "", + "Trim": "xDrive35i", + "Trim2": "", + "Turbo": "", + "VIN": "5UXWX7C5*BA", + "ValveTrainDesign": "", + "VehicleDescriptor": "5UXWX7C5*BA", + "VehicleType": "MULTIPURPOSE PASSENGER VEHICLE (MPV)", + "WheelBaseLong": "", + "WheelBaseShort": "", + "WheelBaseType": "", + "WheelSizeFront": "", + "WheelSizeRear": "", + "WheelieMitigation": "", + "Wheels": "", + "Windows": "" + } + ] +} diff --git a/tests/fixtures/decode_vin_values_extended.json b/tests/fixtures/decode_vin_values_extended.json new file mode 100644 index 0000000..fa42e7a --- /dev/null +++ b/tests/fixtures/decode_vin_values_extended.json @@ -0,0 +1,163 @@ +{ + "Count": 1, + "Message": "Results returned successfully. NOTE: Any missing decoded values should be interpreted as NHTSA does not have data on the specific variable. Missing value should NOT be interpreted as an indication that a feature or technology is unavailable for a vehicle.", + "SearchCriteria": "VIN(s): 5UXWX7C5*BA", + "Results": [ + { + "ABS": "", + "ActiveSafetySysNote": "", + "AdaptiveCruiseControl": "", + "AdaptiveDrivingBeam": "", + "AdaptiveHeadlights": "", + "AdditionalErrorText": "", + "AirBagLocCurtain": "", + "AirBagLocFront": "1st Row (Driver and Passenger)", + "AirBagLocKnee": "", + "AirBagLocSeatCushion": "", + "AirBagLocSide": "1st Row (Driver and Passenger)", + "AutoReverseSystem": "", + "AutomaticPedestrianAlertingSound": "", + "AxleConfiguration": "", + "Axles": "", + "BasePrice": "", + "BatteryA": "", + "BatteryA_to": "", + "BatteryCells": "", + "BatteryInfo": "", + "BatteryKWh": "", + "BatteryKWh_to": "", + "BatteryModules": "", + "BatteryPacks": "", + "BatteryType": "", + "BatteryV": "", + "BatteryV_to": "", + "BedLengthIN": "", + "BedType": "", + "BlindSpotIntervention": "", + "BlindSpotMon": "", + "BodyCabType": "", + "BodyClass": "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]", + "BrakeSystemDesc": "", + "BrakeSystemType": "", + "BusFloorConfigType": "Not Applicable", + "BusLength": "", + "BusType": "Not Applicable", + "CAN_AACN": "", + "CIB": "", + "CashForClunkers": "", + "ChargerLevel": "", + "ChargerPowerKW": "", + "CombinedBrakingSystem": "", + "CoolingType": "", + "CurbWeightLB": "", + "CustomMotorcycleType": "Not Applicable", + "DaytimeRunningLight": "", + "DestinationMarket": "", + "DisplacementCC": "2979.1682352", + "DisplacementCI": "181.8", + "DisplacementL": "3.0", + "Doors": "4", + "DriveType": "AWD/All-Wheel Drive", + "DriverAssist": "", + "DynamicBrakeSupport": "", + "EDR": "", + "ESC": "", + "EVDriveUnit": "", + "ElectrificationLevel": "", + "EngineConfiguration": "", + "EngineCycles": "", + "EngineCylinders": "6", + "EngineHP": "300", + "EngineHP_to": "", + "EngineKW": "", + "EngineManufacturer": "", + "EngineModel": "", + "EntertainmentSystem": "", + "ErrorCode": "6", + "ErrorText": "6 - Incomplete VIN", + "ForwardCollisionWarning": "", + "FuelInjectionType": "", + "FuelTankMaterial": "", + "FuelTankType": "", + "FuelTypePrimary": "Gasoline", + "FuelTypeSecondary": "", + "GCWR": "", + "GCWR_to": "", + "GVWR": "Class 1D: 5,001 - 6,000 lb (2,268 - 2,722 kg)", + "GVWR_to": "", + "KeylessIgnition": "", + "LaneCenteringAssistance": "", + "LaneDepartureWarning": "", + "LaneKeepSystem": "", + "LowerBeamHeadlampLightSource": "", + "Make": "BMW", + "MakeID": "452", + "Manufacturer": "BMW MANUFACTURER CORPORATION / BMW NORTH AMERICA", + "ManufacturerId": "968", + "Model": "X3", + "ModelID": "1719", + "ModelYear": "2011", + "MotorcycleChassisType": "Not Applicable", + "MotorcycleSuspensionType": "Not Applicable", + "NCSABodyType": "Compact Utility (Utility Vehicle Categories \"Small\" and \"Midsize\")", + "NCSAMake": "BMW", + "NCSAMapExcApprovedBy": "", + "NCSAMapExcApprovedOn": "", + "NCSAMappingException": "", + "NCSAModel": "X3", + "NCSANote": "", + "NonLandUse": "", + "Note": "", + "OtherBusInfo": "", + "OtherEngineInfo": "", + "OtherMotorcycleInfo": "", + "OtherRestraintSystemInfo": "Head Inflatable Restraint for Driver, Front Passenger, Rear Outboard Driver-side and Rear Outboard Passenger-side. Knee Inflatable Restraint for Driver. Pretensioners for Driver and Front Passenger.", + "OtherTrailerInfo": "", + "ParkAssist": "", + "PedestrianAutomaticEmergencyBraking": "", + "PlantCity": "MUNICH", + "PlantCompanyName": "", + "PlantCountry": "GERMANY", + "PlantState": "", + "PossibleValues": "", + "Pretensioner": "Yes", + "RearAutomaticEmergencyBraking": "", + "RearCrossTrafficAlert": "", + "RearVisibilitySystem": "", + "SAEAutomationLevel": "", + "SAEAutomationLevel_to": "", + "SeatBeltsAll": "Manual", + "SeatRows": "", + "Seats": "", + "SemiautomaticHeadlampBeamSwitching": "", + "Series": "", + "Series2": "", + "SteeringLocation": "", + "SuggestedVIN": "", + "TPMS": "Direct", + "TopSpeedMPH": "", + "TrackWidth": "", + "TractionControl": "", + "TrailerBodyType": "Not Applicable", + "TrailerLength": "", + "TrailerType": "Not Applicable", + "TransmissionSpeeds": "", + "TransmissionStyle": "", + "Trim": "xDrive35i", + "Trim2": "", + "Turbo": "", + "VIN": "5UXWX7C5*BA", + "ValveTrainDesign": "", + "VehicleDescriptor": "5UXWX7C5*BA", + "VehicleType": "MULTIPURPOSE PASSENGER VEHICLE (MPV)", + "WheelBaseLong": "", + "WheelBaseShort": "", + "WheelBaseType": "", + "WheelSizeFront": "", + "WheelSizeRear": "", + "WheelieMitigation": "", + "Wheels": "", + "Windows": "" + } + ] +} diff --git a/tests/fixtures/decode_wmi.json b/tests/fixtures/decode_wmi.json new file mode 100644 index 0000000..97a9f5e --- /dev/null +++ b/tests/fixtures/decode_wmi.json @@ -0,0 +1,18 @@ +{ + "Count": 1, + "Message": "Results returned successfully", + "SearchCriteria": "WMI:1FD", + "Results": [ + { + "CommonName": "Ford", + "CreatedOn": "2015-03-23", + "DateAvailableToPublic": "2015-01-01", + "Make": "FORD", + "ManufacturerName": "FORD MOTOR COMPANY", + "ParentCompanyName": "", + "URL": "http://www.ford.com/", + "UpdatedOn": null, + "VehicleType": "Incomplete Vehicle" + } + ] +} diff --git a/tests/fixtures/equipment_plant_codes.json b/tests/fixtures/equipment_plant_codes.json new file mode 100644 index 0000000..36415c3 --- /dev/null +++ b/tests/fixtures/equipment_plant_codes.json @@ -0,0 +1,117 @@ +{ + "Count": 936, + "Message": "Results returned successfully", + "SearchCriteria": "Year:2015 | Equipment Type:1 | Report Type: New", + "Results": [ + { + "Address": "2950 International Blvd.", + "City": "Clarksville", + "Country": "UNITED STATES (USA)", + "DOTCode": "00T", + "Name": "Hankook Tire Manufacturing Tennessee, LP", + "OldDotCode": "", + "PostalCode": "37040", + "StateProvince": "TN", + "Status": "" + }, + { + "Address": "No. 52 Street 536, Bau Tran Hamlet, Nhuan Duc Commune, Cu Chi District ", + "City": "Ho Chi Minh", + "Country": "Vietnam", + "DOTCode": "030", + "Name": "SHIH FA RUBBER INDUSTRIES (VN) CO., LTD.", + "OldDotCode": "", + "PostalCode": null, + "StateProvince": null, + "Status": "" + }, + { + "Address": "NO. 23, HAILAR EAST ROAD", + "City": "HUHHOT", + "Country": "CHINA", + "DOTCode": "18V", + "Name": "CHINA HUHHOT RUBBER PLANT", + "OldDotCode": "8V", + "PostalCode": null, + "StateProvince": "INNER MONGOLIA", + "Status": "" + }, + { + "Address": "NO. 9 EAST BEISAN ROAD", + "City": "SHENYANG", + "Country": "CHINA", + "DOTCode": "17H", + "Name": "SHENYANG SUNSHINE TYRE CO., LTD.", + "OldDotCode": "7H", + "PostalCode": "110025", + "StateProvince": "LIAONING PROVINCE", + "Status": "" + }, + { + "Address": "HONGSHANZUI ECONOMIC DEV. ZONE", + "City": "PINGQUAN", + "Country": "CHINA", + "DOTCode": "181", + "Name": "CHENGDE XIANGYAN TYRE CORP. LTD.", + "OldDotCode": "81", + "PostalCode": "067500", + "StateProvince": "HEBEI", + "Status": "" + }, + { + "Address": "QIANLIU VILLAGE XIADIAN TOWN", + "City": "CHANGYI CITY", + "Country": "CHINA", + "DOTCode": "1TN", + "Name": "WEIFANG LUYI RUBBER PRODUCTS CO., LTD.", + "OldDotCode": "TN", + "PostalCode": "261311", + "StateProvince": "SHANDONG", + "Status": "" + }, + { + "Address": "DOWANG TOWN", + "City": "GUANGRAO COUNTY", + "Country": "CHINA", + "DOTCode": "183", + "Name": "SHANDONG HENGFENG RUBBER & PLASTIC CO., LTD.", + "OldDotCode": "83", + "PostalCode": "257335", + "StateProvince": "SHANDONG", + "Status": "" + }, + { + "Address": "NO.1 HUIXIN ROAD", + "City": "GAOTANG", + "Country": "CHINA", + "DOTCode": "184", + "Name": "SHIFENG JUXING TIRE CO., LTD.", + "OldDotCode": "84", + "PostalCode": "252800", + "StateProvince": "SHANDONG PROVINCE", + "Status": "" + }, + { + "Address": "NO.668 LAMEI ROAD,JIESHI TOWN,BANAN DISTRICT", + "City": "CHONGQING", + "Country": "CHINA", + "DOTCode": "185", + "Name": "CHONGQING WEIXING RUBBER INDUSTRY CO., LTD.", + "OldDotCode": "85", + "PostalCode": "401346", + "StateProvince": "CHONGQING", + "Status": "" + }, + { + "Address": "DAWANG", + "City": "GUANGRAO", + "Country": "CHINA", + "DOTCode": "187", + "Name": "SHANDONG HONGSHENG RUBBER CO., LTD.", + "OldDotCode": "87", + "PostalCode": "257335", + "StateProvince": "SHANDONG", + "Status": "" + } + ] +} diff --git a/tests/fixtures/makes_for_manufacturer.json b/tests/fixtures/makes_for_manufacturer.json new file mode 100644 index 0000000..c2a7722 --- /dev/null +++ b/tests/fixtures/makes_for_manufacturer.json @@ -0,0 +1,22 @@ +{ + "Count": 3, + "Message": "Results returned successfully", + "SearchCriteria": "Manufacturer:987", + "Results": [ + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Mfr_Name": "HONDA MOTOR CO., LTD." + }, + { + "Make_ID": 475, + "Make_Name": "ACURA", + "Mfr_Name": "HONDA MOTOR CO., LTD." + }, + { + "Make_ID": 542, + "Make_Name": "ISUZU", + "Mfr_Name": "HONDA MOTOR CO., LTD." + } + ] +} diff --git a/tests/fixtures/makes_for_manufacturer_year.json b/tests/fixtures/makes_for_manufacturer_year.json new file mode 100644 index 0000000..da1d7d3 --- /dev/null +++ b/tests/fixtures/makes_for_manufacturer_year.json @@ -0,0 +1,19 @@ +{ + "Count": 2, + "Message": "Response returned successfully", + "SearchCriteria": "Manufacturer : 987, Year: 2020", + "Results": [ + { + "MakeId": 474, + "MakeName": "HONDA", + "MfrId": 987, + "MfrName": "HONDA MOTOR CO., LTD." + }, + { + "MakeId": 475, + "MakeName": "ACURA", + "MfrId": 987, + "MfrName": "HONDA MOTOR CO., LTD." + } + ] +} diff --git a/tests/fixtures/makes_for_vehicle_type.json b/tests/fixtures/makes_for_vehicle_type.json new file mode 100644 index 0000000..d21f0fe --- /dev/null +++ b/tests/fixtures/makes_for_vehicle_type.json @@ -0,0 +1,79 @@ +{ + "Count": 195, + "Message": "Response returned successfully", + "SearchCriteria": "Vehicle Type: car", + "Results": [ + { + "MakeId": 440, + "MakeName": "ASTON MARTIN", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 441, + "MakeName": "TESLA", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 442, + "MakeName": "JAGUAR", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 443, + "MakeName": "MASERATI", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 445, + "MakeName": "ROLLS-ROYCE", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 448, + "MakeName": "TOYOTA", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 452, + "MakeName": "BMW", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 454, + "MakeName": "BUGATTI", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 456, + "MakeName": "MINI", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 460, + "MakeName": "FORD", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 464, + "MakeName": "LINCOLN", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + } + ] +} diff --git a/tests/fixtures/manufacturer_details.json b/tests/fixtures/manufacturer_details.json new file mode 100644 index 0000000..67df90b --- /dev/null +++ b/tests/fixtures/manufacturer_details.json @@ -0,0 +1,57 @@ +{ + "Count": 1, + "Message": "Response returned successfully", + "SearchCriteria": null, + "Results": [ + { + "Address": "2-3, Toranomon 2-Chome", + "Address2": null, + "City": "Minato-Ku", + "ContactEmail": "kazutomo_sawamura@jp.honda", + "ContactFax": null, + "ContactPhone": "+81 80 4888 7109", + "Country": "JAPAN", + "DBAs": null, + "EquipmentItems": [], + "LastUpdated": "/Date(1781312583703-0400)/", + "ManufacturerTypes": [ + { + "Name": "Completed Vehicle Manufacturer" + } + ], + "Mfr_CommonName": "Honda", + "Mfr_ID": 987, + "Mfr_Name": "HONDA MOTOR CO., LTD.", + "OtherManufacturerDetails": null, + "PostalCode": "105-8404", + "PrimaryProduct": null, + "PrincipalFirstName": "Toshihiro Mibe", + "PrincipalLastName": null, + "PrincipalPosition": "President & CEO", + "StateProvince": "Tokyo", + "SubmittedName": "Liza Copeland", + "SubmittedOn": "/Date(1750910400000-0400)/", + "SubmittedPosition": "Administratiive Assistant", + "VehicleTypes": [ + { + "GVWRFrom": "Class 1A: 3,000 lb or less (1,360 kg or less)", + "GVWRTo": "Class 1: 6,000 lb or less (2,722 kg or less)", + "IsPrimary": false, + "Name": "Motorcycle" + }, + { + "GVWRFrom": "Class 1B: 3,001 - 4,000 lb (1,360 - 1,814 kg)", + "GVWRTo": "Class 1: 6,000 lb or less (2,722 kg or less)", + "IsPrimary": false, + "Name": "Passenger Car" + }, + { + "GVWRFrom": "Class 1: 6,000 lb or less (2,722 kg or less)", + "GVWRTo": "Class 2: 6,001 - 10,000 lb (2,722 - 4,536 kg)", + "IsPrimary": false, + "Name": "Multipurpose Passenger Vehicle (MPV)" + } + ] + } + ] +} diff --git a/tests/fixtures/models_for_make.json b/tests/fixtures/models_for_make.json new file mode 100644 index 0000000..0da07ba --- /dev/null +++ b/tests/fixtures/models_for_make.json @@ -0,0 +1,79 @@ +{ + "Count": 362, + "Message": "Response returned successfully", + "SearchCriteria": "Make:honda", + "Results": [ + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1861, + "Model_Name": "Accord" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1863, + "Model_Name": "Civic" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1864, + "Model_Name": "Pilot" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1865, + "Model_Name": "CR-V" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1866, + "Model_Name": "Ridgeline" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1868, + "Model_Name": "Element" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1869, + "Model_Name": "Odyssey" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1870, + "Model_Name": "Insight" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1875, + "Model_Name": "FCX Clarity" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 2127, + "Model_Name": "Fit" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 2128, + "Model_Name": "CR-Z" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 3153, + "Model_Name": "S2000" + } + ] +} diff --git a/tests/fixtures/models_for_make_id.json b/tests/fixtures/models_for_make_id.json new file mode 100644 index 0000000..43f5014 --- /dev/null +++ b/tests/fixtures/models_for_make_id.json @@ -0,0 +1,79 @@ +{ + "Count": 361, + "Message": "Response returned successfully", + "SearchCriteria": "MakeId:474", + "Results": [ + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1861, + "Model_Name": "Accord" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1863, + "Model_Name": "Civic" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1864, + "Model_Name": "Pilot" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1865, + "Model_Name": "CR-V" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1866, + "Model_Name": "Ridgeline" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1868, + "Model_Name": "Element" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1869, + "Model_Name": "Odyssey" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1870, + "Model_Name": "Insight" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 1875, + "Model_Name": "FCX Clarity" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 2127, + "Model_Name": "Fit" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 2128, + "Model_Name": "CR-Z" + }, + { + "Make_ID": 474, + "Make_Name": "Honda", + "Model_ID": 3153, + "Model_Name": "S2000" + } + ] +} diff --git a/tests/fixtures/models_for_make_year.json b/tests/fixtures/models_for_make_year.json new file mode 100644 index 0000000..4cff4f4 --- /dev/null +++ b/tests/fixtures/models_for_make_year.json @@ -0,0 +1,79 @@ +{ + "Count": 62, + "Message": "Results returned successfully", + "SearchCriteria": "Make:honda | ModelYear:2015", + "Results": [ + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1861, + "Model_Name": "Accord" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1863, + "Model_Name": "Civic" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1864, + "Model_Name": "Pilot" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1865, + "Model_Name": "CR-V" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1866, + "Model_Name": "Ridgeline" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 1869, + "Model_Name": "Odyssey" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 2127, + "Model_Name": "Fit" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 2128, + "Model_Name": "CR-Z" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 3235, + "Model_Name": "CB1100" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 3246, + "Model_Name": "CTX1300" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 3247, + "Model_Name": "CTX700" + }, + { + "Make_ID": 474, + "Make_Name": "HONDA", + "Model_ID": 3249, + "Model_Name": "Gold Wing" + } + ] +} diff --git a/tests/fixtures/parts.json b/tests/fixtures/parts.json new file mode 100644 index 0000000..90dc0a0 --- /dev/null +++ b/tests/fixtures/parts.json @@ -0,0 +1,117 @@ +{ + "Count": 172, + "Message": "Results returned successfully", + "SearchCriteria": "Type: 565 | From Date: 1/1/2015 | To Date: 5/1/2015 | manufacturer: n/a", + "Results": [ + { + "CoverLetterURL": "", + "LetterDate": "4/30/2015", + "ManufacturerId": 987, + "ManufacturerName": "HONDA MOTOR CO., LTD.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10655", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/cf88fcd6-97e5-4b9a-bc6c-53f87eaf4ab3" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/29/2015", + "ManufacturerId": 5368, + "ManufacturerName": "FIRE PLUS, LLC.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10922", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/e678c552-ce71-4531-bcae-e7005f41548b" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/29/2015", + "ManufacturerId": 13712, + "ManufacturerName": "APLUS TRAILERS INC.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10932", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/34df749b-51d1-4621-8f2b-e761f71d1f4d" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/29/2015", + "ManufacturerId": 13777, + "ManufacturerName": "RIVER STEEL, INC.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10762", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/448414e8-8cfc-4c64-89f0-6b3600c7aa40" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/28/2015", + "ManufacturerId": 7176, + "ManufacturerName": "PAGANI SPA", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG12010", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/62fc9972-a3c6-4fea-9295-d7873dbf3589" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/27/2015", + "ManufacturerId": 1043, + "ManufacturerName": "MERCEDES-BENZ OF NORTH AMERICA, INC.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10625", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/94b9c15e-78d3-4663-94de-5190af15b865" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/27/2015", + "ManufacturerId": 1043, + "ManufacturerName": "MERCEDES-BENZ OF NORTH AMERICA, INC.", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10626", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/d0734054-3c45-48b3-89fa-7ccca0206bec" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/23/2015", + "ManufacturerId": 1152, + "ManufacturerName": "KAWASAKI MOTORS, LTD", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10618", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/06302467-a2d2-48ac-9e4f-c1df81b7695a" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/23/2015", + "ManufacturerId": 1152, + "ManufacturerName": "KAWASAKI MOTORS, LTD", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10606", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/543c49e3-473f-4197-b8a4-5405ab0d47d4" + }, + { + "CoverLetterURL": "", + "LetterDate": "4/23/2015", + "ManufacturerId": 1152, + "ManufacturerName": "KAWASAKI MOTORS, LTD", + "ModelYearFrom": null, + "ModelYearTo": null, + "Name": "ORG10607", + "Type": "565", + "URL": "http://vpic.nhtsa.dot.gov/mid/home/displayfile/21c56294-ad95-4ffa-b49c-38265a215680" + } + ] +} diff --git a/tests/fixtures/vehicle_types_for_make.json b/tests/fixtures/vehicle_types_for_make.json new file mode 100644 index 0000000..fa45e8f --- /dev/null +++ b/tests/fixtures/vehicle_types_for_make.json @@ -0,0 +1,37 @@ +{ + "Count": 5, + "Message": "Response returned successfully", + "SearchCriteria": "Make: mercedes", + "Results": [ + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 3, + "VehicleTypeName": "Truck" + }, + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 5, + "VehicleTypeName": "Bus" + }, + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 7, + "VehicleTypeName": "Multipurpose Passenger Vehicle (MPV)" + }, + { + "MakeId": 449, + "MakeName": "MERCEDES-BENZ", + "VehicleTypeId": 10, + "VehicleTypeName": "Incomplete Vehicle" + } + ] +} diff --git a/tests/fixtures/vehicle_types_for_make_id.json b/tests/fixtures/vehicle_types_for_make_id.json new file mode 100644 index 0000000..5ee02f0 --- /dev/null +++ b/tests/fixtures/vehicle_types_for_make_id.json @@ -0,0 +1,27 @@ +{ + "Count": 5, + "Message": "Response returned successfully", + "SearchCriteria": "Make ID: 449", + "Results": [ + { + "VehicleTypeId": 2, + "VehicleTypeName": "Passenger Car" + }, + { + "VehicleTypeId": 3, + "VehicleTypeName": "Truck" + }, + { + "VehicleTypeId": 5, + "VehicleTypeName": "Bus" + }, + { + "VehicleTypeId": 7, + "VehicleTypeName": "Multipurpose Passenger Vehicle (MPV)" + }, + { + "VehicleTypeId": 10, + "VehicleTypeName": "Incomplete Vehicle" + } + ] +} diff --git a/tests/fixtures/vehicle_variable_list.json b/tests/fixtures/vehicle_variable_list.json new file mode 100644 index 0000000..bc80e21 --- /dev/null +++ b/tests/fixtures/vehicle_variable_list.json @@ -0,0 +1,112 @@ +{ + "Count": 144, + "Message": "Results returned successfully", + "SearchCriteria": null, + "Results": [ + { + "DataType": "string", + "Description": "

This field stores any other battery information that does not belong to any of the other battery related fields.

", + "GroupName": "Mechanical/Battery", + "ID": 1, + "Name": "Other Battery Info" + }, + { + "DataType": "lookup", + "Description": "

Battery type field stores the battery chemistry type for anode, cathode.

", + "GroupName": "Mechanical/Battery", + "ID": 2, + "Name": "Battery Type" + }, + { + "DataType": "lookup", + "Description": "

Bed type is the type of bed (the open back) used for pickup trucks. The common values are standard, short, long, extended.

", + "GroupName": "Exterior/Truck", + "ID": 3, + "Name": "Bed Type" + }, + { + "DataType": "lookup", + "Description": "

Cab type applies to both pickup truck and other medium- and heavy-duty trucks. The cab or cabin of a truck is the inside space in a truck where the driver is seated. For pickup trucks, the cab type is categorized by the combination number of doors and number of rows for seating. For medium- and heavy-duty trucks (MDHD), the cab type is categorized by the relative location of engine and cab.

For pickup trucks, there are four cab types.

  • Regular: 2 door, 1 row of seats
  • Extra/Super/Quad/Double/King/Extended: 2 doors, 2 rows of seats
  • Crew/Super Crew/Crew Max: 4 doors, 2 rows of seats
  • Mega: 4 doors, 2 rows of seats (with a bigger cabin than crew cab type)

For medium- and heavy-duty (MDHD) trucks, there are several categories as listed below.

  • Cab Beside Engine
  • CAE: Cab Above Engine
  • CBE: Cab Behind Engine
  • COE: Cab Over Engine or Flat Nose: Driver sits on top of the front axle and engine
  • LCF: Low Cab Forward
  • Conventional: Driver sits behind the engine
  • Non-Tilt
  • Tilt
", + "GroupName": "Exterior/Truck", + "ID": 4, + "Name": "Cab Type" + }, + { + "DataType": "lookup", + "Description": "

Body Class presents the body type based on 49 CFR 565.12(b): \"Body type means the general configuration or shape of a vehicle distinguished by such characteristics as the number of doors or windows, cargo-carrying features and the roofline (e.g., sedan, fastback, hatchback).\" Definitions are not provided for individual body types in the regulation.

", + "GroupName": "Exterior/Body", + "ID": 5, + "Name": "Body Class" + }, + { + "DataType": "int", + "Description": "

This is a numerical field to store the number of cylinders in an engine. Common values for passenger cars are 4 or 6.

", + "GroupName": "Engine", + "ID": 9, + "Name": "Engine Number of Cylinders" + }, + { + "DataType": "lookup", + "Description": "

Destination Market is the market where the vehicle is intended to be sold.

", + "GroupName": "General", + "ID": 10, + "Name": "Destination Market" + }, + { + "DataType": "decimal", + "Description": "

Engine displacement (in cubic centimeters) is the volume swept by all the pistons inside the cylinders of a reciprocating engine in a single movement from top dead center to bottom dead center.

", + "GroupName": "Engine", + "ID": 11, + "Name": "Displacement (CC)" + }, + { + "DataType": "decimal", + "Description": "

Engine displacement (in cubic inches) is the volume swept by all the pistons inside the cylinders of a reciprocating engine in a single movement from top dead center to bottom dead center.

", + "GroupName": "Engine", + "ID": 12, + "Name": "Displacement (CI)" + }, + { + "DataType": "decimal", + "Description": "

Engine displacement (in liters) is the volume swept by all the pistons inside the cylinders of a reciprocating engine in a single movement from top dead center to bottom dead center.

", + "GroupName": "Engine", + "ID": 13, + "Name": "Displacement (L)" + }, + { + "DataType": "int", + "Description": "

This is a numerical field to store the number of doors on a vehicle.

", + "GroupName": "Exterior/Body", + "ID": 14, + "Name": "Doors" + }, + { + "DataType": "lookup", + "Description": "

Drive type stores information about vehicle drivetrain configuration. The most common drive types for passenger cars, crossover vehicles, and pickup trucks are front-wheel drive (FWD), rear-wheel drive (RWD), all-wheel drive (AWD), and 4-wheel drive (4WD).

", + "GroupName": "Mechanical/Drivetrain", + "ID": 15, + "Name": "Drive Type" + }, + { + "DataType": "int", + "Description": "

Engine stroke cycle is a numerical field for the number of strokes used by an internal combustion engine to complete a power cycle.

", + "GroupName": "Engine", + "ID": 17, + "Name": "Engine Stroke Cycles" + }, + { + "DataType": "string", + "Description": "

Engine model is a name that a manufacturer applies to a family of engine.

", + "GroupName": "Engine", + "ID": 18, + "Name": "Engine Model" + }, + { + "DataType": "decimal", + "Description": "

This field stores engine power in kilowatts (kW).

", + "GroupName": "Engine", + "ID": 21, + "Name": "Engine Power (kW)" + } + ] +} diff --git a/tests/fixtures/vehicle_variable_values.json b/tests/fixtures/vehicle_variable_values.json new file mode 100644 index 0000000..f7af3f9 --- /dev/null +++ b/tests/fixtures/vehicle_variable_values.json @@ -0,0 +1,52 @@ +{ + "Count": 9, + "Message": "Results returned successfully", + "SearchCriteria": "Variable:battery type", + "Results": [ + { + "ElementName": "Battery Type", + "Id": 1, + "Name": "Lead Acid/Lead" + }, + { + "ElementName": "Battery Type", + "Id": 2, + "Name": "Nickel-Metal-Hydride/NiMH" + }, + { + "ElementName": "Battery Type", + "Id": 3, + "Name": "Lithium-Ion/Li-Ion" + }, + { + "ElementName": "Battery Type", + "Id": 4, + "Name": "Cobalt Dioxide/Cobalt" + }, + { + "ElementName": "Battery Type", + "Id": 5, + "Name": "Nickle-Cobalt-Manganese/NCM" + }, + { + "ElementName": "Battery Type", + "Id": 6, + "Name": "Nickle-Cobalt-Aluminum/NCA" + }, + { + "ElementName": "Battery Type", + "Id": 7, + "Name": "Manganese Oxide Spinel/MnO" + }, + { + "ElementName": "Battery Type", + "Id": 8, + "Name": "Iron Phosphate/FePo" + }, + { + "ElementName": "Battery Type", + "Id": 9, + "Name": "Silicon" + } + ] +} diff --git a/tests/fixtures/wmis_for_manufacturer.json b/tests/fixtures/wmis_for_manufacturer.json new file mode 100644 index 0000000..5c1624f --- /dev/null +++ b/tests/fixtures/wmis_for_manufacturer.json @@ -0,0 +1,107 @@ +{ + "Count": 104, + "Message": "Response returned successfully", + "SearchCriteria": "Manufacturer: hon", + "Results": [ + { + "Country": "JAPAN", + "CreatedOn": "2015-04-03", + "DateAvailableToPublic": "2015-01-01", + "Id": 987, + "Name": "HONDA MOTOR CO., LTD.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "JH2" + }, + { + "Country": "UNITED STATES (USA)", + "CreatedOn": "2015-04-03", + "DateAvailableToPublic": "2015-01-01", + "Id": 988, + "Name": "AMERICAN HONDA MOTOR CO., INC.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "1HF" + }, + { + "Country": "BELGIUM", + "CreatedOn": "2015-04-03", + "DateAvailableToPublic": "2015-01-01", + "Id": 988, + "Name": "AMERICAN HONDA MOTOR CO., INC.", + "UpdatedOn": "2016-07-08", + "VehicleType": "Motorcycle", + "WMI": "YC1" + }, + { + "Country": "MEXICO", + "CreatedOn": "2015-04-03", + "DateAvailableToPublic": "2015-01-01", + "Id": 992, + "Name": "HONDA DE MEXICO, S.A. DE C.V.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "3H1" + }, + { + "Country": "ITALY", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 988, + "Name": "AMERICAN HONDA MOTOR CO., INC.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "ZDC" + }, + { + "Country": "THAILAND", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 14337, + "Name": "THAI HONDA CO., LTD.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "MLH" + }, + { + "Country": "CHINA", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 14790, + "Name": "SUNDIRO HONDA MOTORCYCLE CO., LTD.", + "UpdatedOn": "2017-08-01", + "VehicleType": "Motorcycle", + "WMI": "LAL" + }, + { + "Country": "VIETNAM", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 14691, + "Name": "HONDA VIETNAM CO., LTD.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "RLH" + }, + { + "Country": "SPAIN", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 15171, + "Name": "MONTESA HONDA SA", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "VTM" + }, + { + "Country": "CHINA", + "CreatedOn": "2015-06-04", + "DateAvailableToPublic": "2015-01-01", + "Id": 988, + "Name": "AMERICAN HONDA MOTOR CO., INC.", + "UpdatedOn": "2021-04-14", + "VehicleType": "Motorcycle", + "WMI": "LWB" + } + ] +}