From b84d4c7fef7fe635f4ff2862d3d6f7dafa729977 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:51:52 -0600 Subject: [PATCH] Vehicle ETL Process fixed. Admin settings fixed. --- .claude/settings.local.json | 3 +- STATION-CHANGES.md | 180 -- .../src/features/admin/api/admin.routes.ts | 46 + .../features/admin/api/catalog.controller.ts | 222 ++ .../admin/domain/catalog-import.service.ts | 476 ++++ .../admin/domain/vehicle-catalog.service.ts | 360 +++ .../002_add_catalog_search_index.sql | 14 + data/source-makes.txt | 53 - data/vehicle-etl/README.md | 41 + .../vehapi_fetch_snapshot.cpython-314.pyc | Bin 33699 -> 33814 bytes data/vehicle-etl/output/01_engines.sql | 140 +- data/vehicle-etl/output/02_transmissions.sql | 21 +- .../vehicle-etl/output/03_vehicle_options.sql | 2196 ++++++++++++++--- data/vehicle-etl/reset_database.sh | 56 + data/vehicle-etl/vehapi_fetch_snapshot.py | 40 +- frontend/src/App.tsx | 80 + frontend/src/core/store/navigation.ts | 2 +- frontend/src/features/admin/api/admin.api.ts | 73 + .../src/features/admin/hooks/useCatalog.ts | 127 + .../admin/mobile/AdminCatalogMobileScreen.tsx | 1343 ++++------ .../src/features/admin/types/admin.types.ts | 62 + .../settings/mobile/MobileSettingsScreen.tsx | 10 +- frontend/src/pages/admin/AdminCatalogPage.tsx | 1458 ++++------- 23 files changed, 4553 insertions(+), 2450 deletions(-) delete mode 100644 STATION-CHANGES.md create mode 100644 backend/src/features/admin/domain/catalog-import.service.ts create mode 100644 backend/src/features/platform/migrations/002_add_catalog_search_index.sql delete mode 100644 data/source-makes.txt create mode 100644 data/vehicle-etl/README.md create mode 100755 data/vehicle-etl/reset_database.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a0e7116..1a64262 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -95,7 +95,8 @@ "Bash(tail:*)", "mcp__playwright__browser_close", "Bash(wc:*)", - "mcp__brave-search__brave_web_search" + "mcp__brave-search__brave_web_search", + "mcp__firecrawl__firecrawl_search" ], "deny": [] } diff --git a/STATION-CHANGES.md b/STATION-CHANGES.md deleted file mode 100644 index 86ce21f..0000000 --- a/STATION-CHANGES.md +++ /dev/null @@ -1,180 +0,0 @@ -# Stations (Gas/Fuel) Feature — Dispatchable Change Plan - -This document is written as an execution plan that can be handed to multiple AI agents to implement in parallel. - -## Repo Constraints (Must Follow) - -- Docker-first workflow (production builds): validate changes via `make rebuild` and container logs. -- Mobile + Desktop requirement: every UI change must be validated on both `frontend/src/features/stations/pages/StationsPage.tsx` (desktop) and `frontend/src/features/stations/mobile/StationsMobileScreen.tsx` (mobile). -- Never expose the Google Maps API key to the browser or logs. - -## Scope - -1. Fix broken station photo rendering on stations UI after the “hide Google API key” change. -2. Add navigation links for saved/favorite stations: - - “Navigate in Google” (Google Maps) - - “Navigate in Apple Maps” (Apple Maps) - - “Navigate in Waze” (Waze) - -## Bug: Station Photos Not Displaying - -### Current Implementation (What Exists Today) - -- Frontend cards render an `` via MUI `CardMedia` when `station.photoReference` is present: - - `frontend/src/features/stations/components/StationCard.tsx` - - URL generation: `frontend/src/features/stations/utils/photo-utils.ts` → `/api/stations/photo/:reference` -- Backend exposes a proxy endpoint that fetches the Google Places photo (server-side, using the secret key): - - Route: `GET /api/stations/photo/:reference` - - `backend/src/features/stations/api/stations.routes.ts` - - `backend/src/features/stations/api/stations.controller.ts` - - Google client: `backend/src/features/stations/external/google-maps/google-maps.client.ts` (`fetchPhoto`) - -### Likely Root Cause (Agents Must Confirm) - -The photo endpoint is protected by `fastify.authenticate`, but `` requests do not include the Authorization header. This results in `401 Unauthorized` responses and broken images. - -Second thing to confirm while debugging: -- Verify what `Station.photoReference` contains at runtime: - - expected: Google `photo_reference` token - - risk: code/docs mismatch where `photoReference` became a URL like `/api/stations/photo/{reference}`, causing double-encoding by `getStationPhotoUrl()`. - -### Repro Checklist (Fast Confirmation) - -- Open stations page and observe broken images in browser devtools Network: - - `GET /api/stations/photo/...` should show `401` if auth-header issue is the cause. -- Confirm backend logs show JWT auth failure for photo requests. - -## Decision: Image Strategy (Selected) - -Selected: **Option A1** (keep images; authenticated blob fetch in frontend; photo endpoint remains JWT-protected). - -### Option A (Keep Images): Fix Auth Mismatch Without Exposing API Key - -#### Option A1 (Recommended): Fetch Photo as Blob via Authenticated XHR - -Why: Keeps `/api/stations/photo/:reference` protected (prevents public key abuse), avoids putting JWT in query params, and avoids exposing the Google API key. - -Implementation outline: -- Frontend: replace direct `` usage with an authenticated fetch that includes the JWT (via existing Axios `apiClient` interceptors), then render via `blob:` object URL. - - Add a small component like `StationPhoto` used by `StationCard`: - - `apiClient.get('/stations/photo/:reference', { responseType: 'blob' })` - - `URL.createObjectURL(blob)` for display - - `URL.revokeObjectURL` cleanup on unmount / reference change - - graceful fallback (hide image) on 401/500 -- Backend: no route auth changes required. - -Tradeoffs: -- Slightly more frontend code, but minimal security risk. -- Must ensure caching behavior is acceptable (browser cache won’t cache `blob:` URLs; rely on backend caching headers + client-side memoization). - -### Option B (Remove Images): Simplify Cards - -Why: If image delivery adds too much complexity or risk, remove images from station cards. - -Implementation outline: -- Frontend: remove `CardMedia` photo block from `StationCard` and any other station photo rendering. -- Leave `photoReference` in API/types untouched for now (or remove later as a cleanup task, separate PR). -- Update any tests that assert on image presence. - -Tradeoffs: -- Reduced UX polish, but simplest and most robust. - -## Feature: Navigation Links on Saved/Favorite Stations - -### UX Requirements - -- On saved station UI (desktop + mobile), provide 3 explicit navigation options: - - Google Maps - - Apple Maps - - Waze -- “Saved/favorite” is interpreted as “stations in the Saved list”; favorites are a subset. - -### URL Construction (Preferred) - -Use coordinates if available; fall back to address query if not. - -- Google Maps: - - Preferred: `https://www.google.com/maps/dir/?api=1&destination=LAT,LNG&destination_place_id=PLACE_ID` - - Fallback: `https://www.google.com/maps/search/?api=1&query=ENCODED_QUERY` -- Apple Maps: - - Preferred: `https://maps.apple.com/?daddr=LAT,LNG` - - Fallback: `https://maps.apple.com/?q=ENCODED_QUERY` -- Waze: - - Preferred: `https://waze.com/ul?ll=LAT,LNG&navigate=yes` - - Fallback: `https://waze.com/ul?q=ENCODED_QUERY&navigate=yes` - -Important: some saved stations may have `latitude/longitude = 0` if cache miss; treat `(0,0)` as “no coordinates”. - -### UI Placement Recommendation - -- Desktop saved list: add a “Navigate” icon button that opens a small menu with the 3 links (cleaner than inline links inside `ListItemText`). - - File: `frontend/src/features/stations/components/SavedStationsList.tsx` -- Mobile bottom sheet (station details): add a “Navigate” section with the same 3 links as buttons. - - File: `frontend/src/features/stations/mobile/StationsMobileScreen.tsx` - -## Work Breakdown for Multiple Agents - -### Agent 1 — Confirm Root Cause + Backend Adjustments (If Needed) - -Deliverables: -- Confirm whether photo requests return `401` due to missing Authorization. -- Confirm whether `photoReference` is a raw reference token vs a URL string. -- Implement backend changes only if Option A2 is chosen. - -Files likely touched (Option A2 only): -- `backend/src/features/stations/api/stations.routes.ts` (remove auth preHandler on photo route) -- `backend/src/features/stations/api/stations.controller.ts` (add stricter validation; keep cache headers) -- `backend/src/features/stations/docs/API.md` (update auth expectations for photo endpoint) - -### Agent 2 — Frontend Photo Fix (Option A1) OR Photo Removal (Option B) - -Deliverables: -- Option A1: implement authenticated blob photo loading for station cards. -- Option B: remove station photos from cards cleanly (no layout regressions). - -Files likely touched: -- `frontend/src/features/stations/components/StationCard.tsx` -- Option A1: - - Add `frontend/src/features/stations/components/StationPhoto.tsx` (or similar) - - Potentially update `frontend/src/features/stations/utils/photo-utils.ts` - - Add unit tests under `frontend/src/features/stations/__tests__/` - -### Agent 3 — Navigation Links for Saved Stations (Desktop + Mobile) - -Deliverables: -- Create a single URL-builder utility with tests. -- Add a “Navigate” menu/section in saved stations UI (desktop + mobile). - -Files likely touched: -- `frontend/src/features/stations/utils/` (new `navigation-links.ts`) -- `frontend/src/features/stations/components/SavedStationsList.tsx` -- `frontend/src/features/stations/mobile/StationsMobileScreen.tsx` -- Optional: reuse in `frontend/src/features/stations/components/StationCard.tsx` (only if product wants it outside Saved) - -### Agent 4 — Tests + QA Pass (Update What Breaks) - -Deliverables: -- Update/extend tests to cover: - - navigation menu/links present for saved stations - - photo rendering behavior per chosen option -- Ensure both desktop and mobile flows still pass basic E2E checks. - -Files likely touched: -- `frontend/cypress/e2e/stations.cy.ts` -- `frontend/src/features/stations/__tests__/components/StationCard.test.tsx` -- New tests for `navigation-links.ts` - -## Acceptance Criteria - -- Station photos render on station cards via Option A1 without exposing Google API key (no `401` responses for photo requests in Network). -- Saved stations show 3 navigation options (Google, Apple, Waze) on both desktop and mobile. -- No lint/test regressions; container build succeeds. - -## Validation (Container-First) - -- Rebuild and watch logs: `make rebuild` then `make logs` -- Optional focused logs: `make logs-frontend` and `make logs-backend` -- Run feature tests where available (prefer container exec): - - Backend: `docker compose exec mvp-backend npm test -- features/stations` - - Frontend: `docker compose exec mvp-frontend npm test -- stations` - - E2E: `docker compose exec mvp-frontend npm run e2e` diff --git a/backend/src/features/admin/api/admin.routes.ts b/backend/src/features/admin/api/admin.routes.ts index 4b8e5a7..929308a 100644 --- a/backend/src/features/admin/api/admin.routes.ts +++ b/backend/src/features/admin/api/admin.routes.ts @@ -20,6 +20,7 @@ import { StationOversightService } from '../domain/station-oversight.service'; import { StationsController } from './stations.controller'; import { CatalogController } from './catalog.controller'; import { VehicleCatalogService } from '../domain/vehicle-catalog.service'; +import { CatalogImportService } from '../domain/catalog-import.service'; import { PlatformCacheService } from '../../platform/domain/platform-cache.service'; import { cacheService } from '../../../core/config/redis'; import { pool } from '../../../core/config/database'; @@ -35,7 +36,9 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => { // Initialize catalog dependencies const platformCacheService = new PlatformCacheService(cacheService); const catalogService = new VehicleCatalogService(pool, platformCacheService); + const catalogImportService = new CatalogImportService(pool); const catalogController = new CatalogController(catalogService); + catalogController.setImportService(catalogImportService); // Admin access verification (used by frontend auth checks) fastify.get('/admin/verify', { @@ -205,6 +208,49 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => { handler: catalogController.getChangeLogs.bind(catalogController) }); + // Search endpoint - full-text search across vehicle_options + fastify.get('/admin/catalog/search', { + preHandler: [fastify.requireAdmin], + handler: catalogController.searchCatalog.bind(catalogController) + }); + + // Cascade delete endpoints - delete entity and all its children + fastify.delete('/admin/catalog/makes/:makeId/cascade', { + preHandler: [fastify.requireAdmin], + handler: catalogController.deleteMakeCascade.bind(catalogController) + }); + + fastify.delete('/admin/catalog/models/:modelId/cascade', { + preHandler: [fastify.requireAdmin], + handler: catalogController.deleteModelCascade.bind(catalogController) + }); + + fastify.delete('/admin/catalog/years/:yearId/cascade', { + preHandler: [fastify.requireAdmin], + handler: catalogController.deleteYearCascade.bind(catalogController) + }); + + fastify.delete('/admin/catalog/trims/:trimId/cascade', { + preHandler: [fastify.requireAdmin], + handler: catalogController.deleteTrimCascade.bind(catalogController) + }); + + // Import/Export endpoints + fastify.post('/admin/catalog/import/preview', { + preHandler: [fastify.requireAdmin], + handler: catalogController.importPreview.bind(catalogController) + }); + + fastify.post('/admin/catalog/import/apply', { + preHandler: [fastify.requireAdmin], + handler: catalogController.importApply.bind(catalogController) + }); + + fastify.get('/admin/catalog/export', { + preHandler: [fastify.requireAdmin], + handler: catalogController.exportCatalog.bind(catalogController) + }); + // Bulk delete endpoint fastify.delete<{ Params: { entity: CatalogEntity }; Body: BulkDeleteCatalogInput }>('/admin/catalog/:entity/bulk-delete', { preHandler: [fastify.requireAdmin], diff --git a/backend/src/features/admin/api/catalog.controller.ts b/backend/src/features/admin/api/catalog.controller.ts index 5b6d69e..e4a9701 100644 --- a/backend/src/features/admin/api/catalog.controller.ts +++ b/backend/src/features/admin/api/catalog.controller.ts @@ -5,11 +5,18 @@ import { FastifyRequest, FastifyReply } from 'fastify'; import { VehicleCatalogService } from '../domain/vehicle-catalog.service'; +import { CatalogImportService } from '../domain/catalog-import.service'; import { logger } from '../../../core/logging/logger'; export class CatalogController { + private importService: CatalogImportService | null = null; + constructor(private catalogService: VehicleCatalogService) {} + setImportService(importService: CatalogImportService): void { + this.importService = importService; + } + // MAKES ENDPOINTS async getMakes(_request: FastifyRequest, reply: FastifyReply): Promise { @@ -593,6 +600,221 @@ export class CatalogController { } } + // SEARCH ENDPOINT + + async searchCatalog( + request: FastifyRequest<{ Querystring: { q?: string; page?: string; pageSize?: string } }>, + reply: FastifyReply + ): Promise { + try { + const query = request.query.q || ''; + const page = parseInt(request.query.page || '1'); + const pageSize = Math.min(parseInt(request.query.pageSize || '50'), 100); // Max 100 per page + + if (isNaN(page) || page < 1) { + reply.code(400).send({ error: 'Invalid page number' }); + return; + } + + if (isNaN(pageSize) || pageSize < 1) { + reply.code(400).send({ error: 'Invalid page size' }); + return; + } + + const result = await this.catalogService.searchCatalog(query, page, pageSize); + reply.code(200).send(result); + } catch (error) { + logger.error('Error searching catalog', { error }); + reply.code(500).send({ error: 'Failed to search catalog' }); + } + } + + // CASCADE DELETE ENDPOINTS + + async deleteMakeCascade( + request: FastifyRequest<{ Params: { makeId: string } }>, + reply: FastifyReply + ): Promise { + try { + const makeId = parseInt(request.params.makeId); + const actorId = request.userContext?.userId || 'unknown'; + + if (isNaN(makeId)) { + reply.code(400).send({ error: 'Invalid make ID' }); + return; + } + + const result = await this.catalogService.deleteMakeCascade(makeId, actorId); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error cascade deleting make', { error }); + if (error.message?.includes('not found')) { + reply.code(404).send({ error: error.message }); + } else { + reply.code(500).send({ error: 'Failed to cascade delete make' }); + } + } + } + + async deleteModelCascade( + request: FastifyRequest<{ Params: { modelId: string } }>, + reply: FastifyReply + ): Promise { + try { + const modelId = parseInt(request.params.modelId); + const actorId = request.userContext?.userId || 'unknown'; + + if (isNaN(modelId)) { + reply.code(400).send({ error: 'Invalid model ID' }); + return; + } + + const result = await this.catalogService.deleteModelCascade(modelId, actorId); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error cascade deleting model', { error }); + if (error.message?.includes('not found')) { + reply.code(404).send({ error: error.message }); + } else { + reply.code(500).send({ error: 'Failed to cascade delete model' }); + } + } + } + + async deleteYearCascade( + request: FastifyRequest<{ Params: { yearId: string } }>, + reply: FastifyReply + ): Promise { + try { + const yearId = parseInt(request.params.yearId); + const actorId = request.userContext?.userId || 'unknown'; + + if (isNaN(yearId)) { + reply.code(400).send({ error: 'Invalid year ID' }); + return; + } + + const result = await this.catalogService.deleteYearCascade(yearId, actorId); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error cascade deleting year', { error }); + if (error.message?.includes('not found')) { + reply.code(404).send({ error: error.message }); + } else { + reply.code(500).send({ error: 'Failed to cascade delete year' }); + } + } + } + + async deleteTrimCascade( + request: FastifyRequest<{ Params: { trimId: string } }>, + reply: FastifyReply + ): Promise { + try { + const trimId = parseInt(request.params.trimId); + const actorId = request.userContext?.userId || 'unknown'; + + if (isNaN(trimId)) { + reply.code(400).send({ error: 'Invalid trim ID' }); + return; + } + + const result = await this.catalogService.deleteTrimCascade(trimId, actorId); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error cascade deleting trim', { error }); + if (error.message?.includes('not found')) { + reply.code(404).send({ error: error.message }); + } else { + reply.code(500).send({ error: 'Failed to cascade delete trim' }); + } + } + } + + // IMPORT/EXPORT ENDPOINTS + + async importPreview( + request: FastifyRequest, + reply: FastifyReply + ): Promise { + try { + if (!this.importService) { + reply.code(500).send({ error: 'Import service not configured' }); + return; + } + + const data = await request.file(); + if (!data) { + reply.code(400).send({ error: 'No file uploaded' }); + return; + } + + const buffer = await data.toBuffer(); + const csvContent = buffer.toString('utf-8'); + + const result = await this.importService.previewImport(csvContent); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error previewing import', { error }); + reply.code(500).send({ error: error.message || 'Failed to preview import' }); + } + } + + async importApply( + request: FastifyRequest<{ Body: { previewId: string } }>, + reply: FastifyReply + ): Promise { + try { + if (!this.importService) { + reply.code(500).send({ error: 'Import service not configured' }); + return; + } + + const { previewId } = request.body; + const actorId = request.userContext?.userId || 'unknown'; + + if (!previewId) { + reply.code(400).send({ error: 'Preview ID is required' }); + return; + } + + const result = await this.importService.applyImport(previewId, actorId); + reply.code(200).send(result); + } catch (error: any) { + logger.error('Error applying import', { error }); + if (error.message?.includes('expired') || error.message?.includes('not found')) { + reply.code(404).send({ error: error.message }); + } else if (error.message?.includes('validation errors')) { + reply.code(400).send({ error: error.message }); + } else { + reply.code(500).send({ error: error.message || 'Failed to apply import' }); + } + } + } + + async exportCatalog( + _request: FastifyRequest, + reply: FastifyReply + ): Promise { + try { + if (!this.importService) { + reply.code(500).send({ error: 'Import service not configured' }); + return; + } + + const csvContent = await this.importService.exportCatalog(); + + reply + .header('Content-Type', 'text/csv') + .header('Content-Disposition', 'attachment; filename="vehicle-catalog.csv"') + .code(200) + .send(csvContent); + } catch (error: any) { + logger.error('Error exporting catalog', { error }); + reply.code(500).send({ error: 'Failed to export catalog' }); + } + } + // BULK DELETE ENDPOINT async bulkDeleteCatalogEntity( diff --git a/backend/src/features/admin/domain/catalog-import.service.ts b/backend/src/features/admin/domain/catalog-import.service.ts new file mode 100644 index 0000000..3d30576 --- /dev/null +++ b/backend/src/features/admin/domain/catalog-import.service.ts @@ -0,0 +1,476 @@ +/** + * @ai-summary Catalog CSV import/export service + * @ai-context Handles bulk import with preview and export of vehicle catalog data + */ + +import { Pool } from 'pg'; +import { logger } from '../../../core/logging/logger'; + +export interface ImportRow { + action: 'add' | 'update' | 'delete'; + year: number; + make: string; + model: string; + trim: string; + engineName: string | null; + transmissionType: string | null; +} + +export interface ImportError { + row: number; + error: string; +} + +export interface ImportPreviewResult { + previewId: string; + toCreate: ImportRow[]; + toUpdate: ImportRow[]; + toDelete: ImportRow[]; + errors: ImportError[]; + valid: boolean; +} + +export interface ImportApplyResult { + created: number; + updated: number; + deleted: number; + errors: ImportError[]; +} + +export interface ExportRow { + year: number; + make: string; + model: string; + trim: string; + engineName: string | null; + transmissionType: string | null; +} + +// In-memory preview cache (expires after 15 minutes) +const previewCache = new Map(); + +export class CatalogImportService { + constructor(private pool: Pool) {} + + /** + * Parse CSV content and validate without applying changes + */ + async previewImport(csvContent: string): Promise { + const previewId = uuidv4(); + const toCreate: ImportRow[] = []; + const toUpdate: ImportRow[] = []; + const toDelete: ImportRow[] = []; + const errors: ImportError[] = []; + + const lines = csvContent.trim().split('\n'); + if (lines.length < 2) { + return { + previewId, + toCreate, + toUpdate, + toDelete, + errors: [{ row: 0, error: 'CSV must have a header row and at least one data row' }], + valid: false, + }; + } + + // Parse header row + const header = this.parseCSVLine(lines[0]); + const headerLower = header.map(h => h.toLowerCase().trim()); + + // Validate required headers + const requiredHeaders = ['action', 'year', 'make', 'model', 'trim']; + for (const required of requiredHeaders) { + if (!headerLower.includes(required)) { + return { + previewId, + toCreate, + toUpdate, + toDelete, + errors: [{ row: 1, error: `Missing required header: ${required}` }], + valid: false, + }; + } + } + + // Find column indices + const colIndices = { + action: headerLower.indexOf('action'), + year: headerLower.indexOf('year'), + make: headerLower.indexOf('make'), + model: headerLower.indexOf('model'), + trim: headerLower.indexOf('trim'), + engineName: headerLower.indexOf('engine_name'), + transmissionType: headerLower.indexOf('transmission_type'), + }; + + // Parse data rows + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + + const values = this.parseCSVLine(line); + const rowNum = i + 1; + + try { + const action = values[colIndices.action]?.toLowerCase().trim(); + const year = parseInt(values[colIndices.year], 10); + const make = values[colIndices.make]?.trim(); + const model = values[colIndices.model]?.trim(); + const trim = values[colIndices.trim]?.trim(); + const engineName = colIndices.engineName >= 0 ? values[colIndices.engineName]?.trim() || null : null; + const transmissionType = colIndices.transmissionType >= 0 ? values[colIndices.transmissionType]?.trim() || null : null; + + // Validate action + if (!['add', 'update', 'delete'].includes(action)) { + errors.push({ row: rowNum, error: `Invalid action: ${action}. Must be add, update, or delete` }); + continue; + } + + // Validate year + if (isNaN(year) || year < 1900 || year > 2100) { + errors.push({ row: rowNum, error: `Invalid year: ${values[colIndices.year]}` }); + continue; + } + + // Validate required fields + if (!make) { + errors.push({ row: rowNum, error: 'Make is required' }); + continue; + } + if (!model) { + errors.push({ row: rowNum, error: 'Model is required' }); + continue; + } + if (!trim) { + errors.push({ row: rowNum, error: 'Trim is required' }); + continue; + } + + const row: ImportRow = { + action: action as 'add' | 'update' | 'delete', + year, + make, + model, + trim, + engineName, + transmissionType, + }; + + // Check if record exists for validation + const existsResult = await this.pool.query( + `SELECT id FROM vehicle_options + WHERE year = $1 AND make = $2 AND model = $3 AND trim = $4 + LIMIT 1`, + [year, make, model, trim] + ); + const exists = (existsResult.rowCount || 0) > 0; + + if (action === 'add' && exists) { + errors.push({ row: rowNum, error: `Record already exists: ${year} ${make} ${model} ${trim}` }); + continue; + } + if ((action === 'update' || action === 'delete') && !exists) { + errors.push({ row: rowNum, error: `Record not found: ${year} ${make} ${model} ${trim}` }); + continue; + } + + // Sort into appropriate bucket + switch (action) { + case 'add': + toCreate.push(row); + break; + case 'update': + toUpdate.push(row); + break; + case 'delete': + toDelete.push(row); + break; + } + } catch (error: any) { + errors.push({ row: rowNum, error: error.message || 'Parse error' }); + } + } + + const result: ImportPreviewResult = { + previewId, + toCreate, + toUpdate, + toDelete, + errors, + valid: errors.length === 0, + }; + + // Cache preview for 15 minutes + previewCache.set(previewId, { + data: result, + expiresAt: Date.now() + 15 * 60 * 1000, + }); + + // Clean up expired previews + this.cleanupExpiredPreviews(); + + return result; + } + + /** + * Apply a previously previewed import + */ + async applyImport(previewId: string, changedBy: string): Promise { + const cached = previewCache.get(previewId); + if (!cached || cached.expiresAt < Date.now()) { + throw new Error('Preview expired or not found. Please upload the file again.'); + } + + const preview = cached.data; + if (!preview.valid) { + throw new Error('Cannot apply import with validation errors'); + } + + const result: ImportApplyResult = { + created: 0, + updated: 0, + deleted: 0, + errors: [], + }; + + const client = await this.pool.connect(); + try { + await client.query('BEGIN'); + + // Process creates + for (const row of preview.toCreate) { + try { + // Get or create engine + let engineId: number | null = null; + if (row.engineName) { + const engineResult = await client.query( + `INSERT INTO engines (name, fuel_type) + VALUES ($1, 'Gas') + ON CONFLICT (LOWER(name)) DO UPDATE SET name = EXCLUDED.name + RETURNING id`, + [row.engineName] + ); + engineId = engineResult.rows[0].id; + } + + // Get or create transmission + let transmissionId: number | null = null; + if (row.transmissionType) { + const transResult = await client.query( + `INSERT INTO transmissions (type) + VALUES ($1) + ON CONFLICT (LOWER(type)) DO UPDATE SET type = EXCLUDED.type + RETURNING id`, + [row.transmissionType] + ); + transmissionId = transResult.rows[0].id; + } + + // Insert vehicle option + await client.query( + `INSERT INTO vehicle_options (year, make, model, trim, engine_id, transmission_id) + VALUES ($1, $2, $3, $4, $5, $6)`, + [row.year, row.make, row.model, row.trim, engineId, transmissionId] + ); + + result.created++; + } catch (error: any) { + result.errors.push({ row: 0, error: `Failed to create ${row.year} ${row.make} ${row.model} ${row.trim}: ${error.message}` }); + } + } + + // Process updates + for (const row of preview.toUpdate) { + try { + // Get or create engine + let engineId: number | null = null; + if (row.engineName) { + const engineResult = await client.query( + `INSERT INTO engines (name, fuel_type) + VALUES ($1, 'Gas') + ON CONFLICT (LOWER(name)) DO UPDATE SET name = EXCLUDED.name + RETURNING id`, + [row.engineName] + ); + engineId = engineResult.rows[0].id; + } + + // Get or create transmission + let transmissionId: number | null = null; + if (row.transmissionType) { + const transResult = await client.query( + `INSERT INTO transmissions (type) + VALUES ($1) + ON CONFLICT (LOWER(type)) DO UPDATE SET type = EXCLUDED.type + RETURNING id`, + [row.transmissionType] + ); + transmissionId = transResult.rows[0].id; + } + + // Update vehicle option + await client.query( + `UPDATE vehicle_options + SET engine_id = $5, transmission_id = $6, updated_at = NOW() + WHERE year = $1 AND make = $2 AND model = $3 AND trim = $4`, + [row.year, row.make, row.model, row.trim, engineId, transmissionId] + ); + + result.updated++; + } catch (error: any) { + result.errors.push({ row: 0, error: `Failed to update ${row.year} ${row.make} ${row.model} ${row.trim}: ${error.message}` }); + } + } + + // Process deletes + for (const row of preview.toDelete) { + try { + await client.query( + `DELETE FROM vehicle_options + WHERE year = $1 AND make = $2 AND model = $3 AND trim = $4`, + [row.year, row.make, row.model, row.trim] + ); + result.deleted++; + } catch (error: any) { + result.errors.push({ row: 0, error: `Failed to delete ${row.year} ${row.make} ${row.model} ${row.trim}: ${error.message}` }); + } + } + + await client.query('COMMIT'); + + // Log the import action + await this.pool.query( + `INSERT INTO platform_change_log (change_type, resource_type, resource_id, old_value, new_value, changed_by) + VALUES ('CREATE', 'import', $1, NULL, $2, $3)`, + [ + previewId, + JSON.stringify({ created: result.created, updated: result.updated, deleted: result.deleted }), + changedBy, + ] + ); + + // Remove preview from cache + previewCache.delete(previewId); + + logger.info('Catalog import completed', { + previewId, + created: result.created, + updated: result.updated, + deleted: result.deleted, + errors: result.errors.length, + changedBy, + }); + + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } + + /** + * Export all vehicle options as CSV + */ + async exportCatalog(): Promise { + const result = await this.pool.query(` + SELECT + vo.year, + vo.make, + vo.model, + vo.trim, + e.name AS engine_name, + t.type AS transmission_type + FROM vehicle_options vo + LEFT JOIN engines e ON vo.engine_id = e.id + LEFT JOIN transmissions t ON vo.transmission_id = t.id + ORDER BY vo.year DESC, vo.make ASC, vo.model ASC, vo.trim ASC + `); + + // Build CSV + const header = 'year,make,model,trim,engine_name,transmission_type'; + const rows = result.rows.map(row => { + return [ + row.year, + this.escapeCSVField(row.make), + this.escapeCSVField(row.model), + this.escapeCSVField(row.trim), + this.escapeCSVField(row.engine_name || ''), + this.escapeCSVField(row.transmission_type || ''), + ].join(','); + }); + + return [header, ...rows].join('\n'); + } + + /** + * Parse a single CSV line, handling quoted fields + */ + private parseCSVLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const nextChar = line[i + 1]; + + if (inQuotes) { + if (char === '"' && nextChar === '"') { + current += '"'; + i++; // Skip next quote + } else if (char === '"') { + inQuotes = false; + } else { + current += char; + } + } else { + if (char === '"') { + inQuotes = true; + } else if (char === ',') { + result.push(current); + current = ''; + } else { + current += char; + } + } + } + + result.push(current); + return result; + } + + /** + * Escape a field for CSV output + */ + private escapeCSVField(value: string): string { + if (!value) return ''; + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + } + + /** + * Clean up expired preview cache entries + */ + private cleanupExpiredPreviews(): void { + const now = Date.now(); + for (const [id, entry] of previewCache.entries()) { + if (entry.expiresAt < now) { + previewCache.delete(id); + } + } + } +} + +// Simple UUID generation without external dependency +function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/backend/src/features/admin/domain/vehicle-catalog.service.ts b/backend/src/features/admin/domain/vehicle-catalog.service.ts index 57ecc80..9bda946 100644 --- a/backend/src/features/admin/domain/vehicle-catalog.service.ts +++ b/backend/src/features/admin/domain/vehicle-catalog.service.ts @@ -60,6 +60,34 @@ export interface PlatformChangeLog { createdAt: Date; } +export interface CatalogSearchResult { + id: number; + year: number; + make: string; + model: string; + trim: string; + engineId: number | null; + engineName: string | null; + transmissionId: number | null; + transmissionType: string | null; +} + +export interface CatalogSearchResponse { + items: CatalogSearchResult[]; + total: number; + page: number; + pageSize: number; +} + +export interface CascadeDeleteResult { + deletedMakes: number; + deletedModels: number; + deletedYears: number; + deletedTrims: number; + deletedEngines: number; + totalDeleted: number; +} + const VEHICLE_SCHEMA = 'vehicles'; export class VehicleCatalogService { @@ -611,6 +639,338 @@ export class VehicleCatalogService { } } + // SEARCH ----------------------------------------------------------------- + + async searchCatalog( + query: string, + page: number = 1, + pageSize: number = 50 + ): Promise { + const offset = (page - 1) * pageSize; + const sanitizedQuery = query.trim(); + + if (!sanitizedQuery) { + return { items: [], total: 0, page, pageSize }; + } + + // Convert query to tsquery format - split words and join with & + const tsQueryTerms = sanitizedQuery + .split(/\s+/) + .filter(term => term.length > 0) + .map(term => term.replace(/[^\w]/g, '')) + .filter(term => term.length > 0) + .map(term => `${term}:*`) + .join(' & '); + + if (!tsQueryTerms) { + return { items: [], total: 0, page, pageSize }; + } + + try { + // Count total matching records + const countQuery = ` + SELECT COUNT(*) as total + FROM vehicle_options vo + WHERE to_tsvector('english', vo.year::text || ' ' || vo.make || ' ' || vo.model || ' ' || vo.trim) + @@ to_tsquery('english', $1) + `; + const countResult = await this.pool.query(countQuery, [tsQueryTerms]); + const total = parseInt(countResult.rows[0].total, 10); + + // Fetch paginated results with engine and transmission details + const searchQuery = ` + SELECT + vo.id, + vo.year, + vo.make, + vo.model, + vo.trim, + vo.engine_id, + e.name AS engine_name, + vo.transmission_id, + t.type AS transmission_type + FROM vehicle_options vo + LEFT JOIN engines e ON vo.engine_id = e.id + LEFT JOIN transmissions t ON vo.transmission_id = t.id + WHERE to_tsvector('english', vo.year::text || ' ' || vo.make || ' ' || vo.model || ' ' || vo.trim) + @@ to_tsquery('english', $1) + ORDER BY vo.year DESC, vo.make ASC, vo.model ASC, vo.trim ASC + LIMIT $2 OFFSET $3 + `; + + const result = await this.pool.query(searchQuery, [tsQueryTerms, pageSize, offset]); + + const items: CatalogSearchResult[] = result.rows.map((row) => ({ + id: Number(row.id), + year: Number(row.year), + make: row.make, + model: row.model, + trim: row.trim, + engineId: row.engine_id ? Number(row.engine_id) : null, + engineName: row.engine_name || null, + transmissionId: row.transmission_id ? Number(row.transmission_id) : null, + transmissionType: row.transmission_type || null, + })); + + return { items, total, page, pageSize }; + } catch (error) { + logger.error('Error searching catalog', { error, query: sanitizedQuery }); + throw error; + } + } + + // CASCADE DELETE METHODS ------------------------------------------------- + + async deleteMakeCascade(makeId: number, changedBy: string): Promise { + const result: CascadeDeleteResult = { + deletedMakes: 0, + deletedModels: 0, + deletedYears: 0, + deletedTrims: 0, + deletedEngines: 0, + totalDeleted: 0, + }; + + await this.runInTransaction(async (client) => { + // Verify make exists + const makeResult = await client.query( + `SELECT id, name, created_at, updated_at FROM ${VEHICLE_SCHEMA}.make WHERE id = $1`, + [makeId] + ); + if (makeResult.rowCount === 0) { + throw new Error(`Make ${makeId} not found`); + } + const makeData = this.mapMakeRow(makeResult.rows[0]); + + // Get all models for this make + const modelsResult = await client.query( + `SELECT id FROM ${VEHICLE_SCHEMA}.model WHERE make_id = $1`, + [makeId] + ); + + // Cascade delete all models and their children + for (const modelRow of modelsResult.rows) { + const modelDeletes = await this.deleteModelCascadeInTransaction(client, modelRow.id, changedBy); + result.deletedModels += modelDeletes.deletedModels; + result.deletedYears += modelDeletes.deletedYears; + result.deletedTrims += modelDeletes.deletedTrims; + result.deletedEngines += modelDeletes.deletedEngines; + } + + // Delete the make itself + await client.query(`DELETE FROM ${VEHICLE_SCHEMA}.make WHERE id = $1`, [makeId]); + await this.logChange(client, 'DELETE', 'makes', makeId.toString(), makeData, null, changedBy); + result.deletedMakes = 1; + }); + + result.totalDeleted = result.deletedMakes + result.deletedModels + result.deletedYears + result.deletedTrims + result.deletedEngines; + await this.cacheService.invalidateVehicleData(); + return result; + } + + async deleteModelCascade(modelId: number, changedBy: string): Promise { + const result: CascadeDeleteResult = { + deletedMakes: 0, + deletedModels: 0, + deletedYears: 0, + deletedTrims: 0, + deletedEngines: 0, + totalDeleted: 0, + }; + + await this.runInTransaction(async (client) => { + const deletes = await this.deleteModelCascadeInTransaction(client, modelId, changedBy); + result.deletedModels = deletes.deletedModels; + result.deletedYears = deletes.deletedYears; + result.deletedTrims = deletes.deletedTrims; + result.deletedEngines = deletes.deletedEngines; + }); + + result.totalDeleted = result.deletedModels + result.deletedYears + result.deletedTrims + result.deletedEngines; + await this.cacheService.invalidateVehicleData(); + return result; + } + + async deleteYearCascade(yearId: number, changedBy: string): Promise { + const result: CascadeDeleteResult = { + deletedMakes: 0, + deletedModels: 0, + deletedYears: 0, + deletedTrims: 0, + deletedEngines: 0, + totalDeleted: 0, + }; + + await this.runInTransaction(async (client) => { + const deletes = await this.deleteYearCascadeInTransaction(client, yearId, changedBy); + result.deletedYears = deletes.deletedYears; + result.deletedTrims = deletes.deletedTrims; + result.deletedEngines = deletes.deletedEngines; + }); + + result.totalDeleted = result.deletedYears + result.deletedTrims + result.deletedEngines; + await this.cacheService.invalidateVehicleData(); + return result; + } + + async deleteTrimCascade(trimId: number, changedBy: string): Promise { + const result: CascadeDeleteResult = { + deletedMakes: 0, + deletedModels: 0, + deletedYears: 0, + deletedTrims: 0, + deletedEngines: 0, + totalDeleted: 0, + }; + + await this.runInTransaction(async (client) => { + const deletes = await this.deleteTrimCascadeInTransaction(client, trimId, changedBy); + result.deletedTrims = deletes.deletedTrims; + result.deletedEngines = deletes.deletedEngines; + }); + + result.totalDeleted = result.deletedTrims + result.deletedEngines; + await this.cacheService.invalidateVehicleData(); + return result; + } + + // Private cascade helpers (run within existing transaction) + + private async deleteModelCascadeInTransaction( + client: PoolClient, + modelId: number, + changedBy: string + ): Promise<{ deletedModels: number; deletedYears: number; deletedTrims: number; deletedEngines: number }> { + const result = { deletedModels: 0, deletedYears: 0, deletedTrims: 0, deletedEngines: 0 }; + + // Verify model exists + const modelResult = await client.query( + `SELECT id, make_id, name, created_at, updated_at FROM ${VEHICLE_SCHEMA}.model WHERE id = $1`, + [modelId] + ); + if (modelResult.rowCount === 0) { + throw new Error(`Model ${modelId} not found`); + } + const modelData = this.mapModelRow(modelResult.rows[0]); + + // Get all years for this model + const yearsResult = await client.query( + `SELECT id FROM ${VEHICLE_SCHEMA}.model_year WHERE model_id = $1`, + [modelId] + ); + + // Cascade delete all years and their children + for (const yearRow of yearsResult.rows) { + const yearDeletes = await this.deleteYearCascadeInTransaction(client, yearRow.id, changedBy); + result.deletedYears += yearDeletes.deletedYears; + result.deletedTrims += yearDeletes.deletedTrims; + result.deletedEngines += yearDeletes.deletedEngines; + } + + // Delete the model itself + await client.query(`DELETE FROM ${VEHICLE_SCHEMA}.model WHERE id = $1`, [modelId]); + await this.logChange(client, 'DELETE', 'models', modelId.toString(), modelData, null, changedBy); + result.deletedModels = 1; + + return result; + } + + private async deleteYearCascadeInTransaction( + client: PoolClient, + yearId: number, + changedBy: string + ): Promise<{ deletedYears: number; deletedTrims: number; deletedEngines: number }> { + const result = { deletedYears: 0, deletedTrims: 0, deletedEngines: 0 }; + + // Verify year exists + const yearResult = await client.query( + `SELECT id, model_id, year, created_at, updated_at FROM ${VEHICLE_SCHEMA}.model_year WHERE id = $1`, + [yearId] + ); + if (yearResult.rowCount === 0) { + throw new Error(`Year ${yearId} not found`); + } + const yearData = this.mapYearRow(yearResult.rows[0]); + + // Get all trims for this year + const trimsResult = await client.query( + `SELECT id FROM ${VEHICLE_SCHEMA}.trim WHERE model_year_id = $1`, + [yearId] + ); + + // Cascade delete all trims and their engines + for (const trimRow of trimsResult.rows) { + const trimDeletes = await this.deleteTrimCascadeInTransaction(client, trimRow.id, changedBy); + result.deletedTrims += trimDeletes.deletedTrims; + result.deletedEngines += trimDeletes.deletedEngines; + } + + // Delete the year itself + await client.query(`DELETE FROM ${VEHICLE_SCHEMA}.model_year WHERE id = $1`, [yearId]); + await this.logChange(client, 'DELETE', 'years', yearId.toString(), yearData, null, changedBy); + result.deletedYears = 1; + + return result; + } + + private async deleteTrimCascadeInTransaction( + client: PoolClient, + trimId: number, + changedBy: string + ): Promise<{ deletedTrims: number; deletedEngines: number }> { + const result = { deletedTrims: 0, deletedEngines: 0 }; + + // Verify trim exists + const trimResult = await client.query( + `SELECT id, model_year_id, name, created_at, updated_at FROM ${VEHICLE_SCHEMA}.trim WHERE id = $1`, + [trimId] + ); + if (trimResult.rowCount === 0) { + throw new Error(`Trim ${trimId} not found`); + } + const trimData = this.mapTrimRow(trimResult.rows[0]); + + // Get all engines linked to this trim + const enginesResult = await client.query( + `SELECT e.id, e.name, e.displacement_l, e.cylinders, e.fuel_type, e.created_at, e.updated_at, te.trim_id + FROM ${VEHICLE_SCHEMA}.engine e + JOIN ${VEHICLE_SCHEMA}.trim_engine te ON te.engine_id = e.id + WHERE te.trim_id = $1`, + [trimId] + ); + + // Delete engine links and potentially orphaned engines + for (const engineRow of enginesResult.rows) { + const engineData = this.mapEngineRow(engineRow); + + // Remove the link first + await client.query( + `DELETE FROM ${VEHICLE_SCHEMA}.trim_engine WHERE trim_id = $1 AND engine_id = $2`, + [trimId, engineRow.id] + ); + + // Check if this engine is used by other trims + const otherLinksResult = await client.query( + `SELECT 1 FROM ${VEHICLE_SCHEMA}.trim_engine WHERE engine_id = $1 LIMIT 1`, + [engineRow.id] + ); + + // If no other trims use this engine, delete the engine itself + if ((otherLinksResult.rowCount || 0) === 0) { + await client.query(`DELETE FROM ${VEHICLE_SCHEMA}.engine WHERE id = $1`, [engineRow.id]); + await this.logChange(client, 'DELETE', 'engines', engineRow.id.toString(), engineData, null, changedBy); + result.deletedEngines += 1; + } + } + + // Delete the trim itself + await client.query(`DELETE FROM ${VEHICLE_SCHEMA}.trim WHERE id = $1`, [trimId]); + await this.logChange(client, 'DELETE', 'trims', trimId.toString(), trimData, null, changedBy); + result.deletedTrims = 1; + + return result; + } + // HELPERS ---------------------------------------------------------------- private async runInTransaction(handler: (client: PoolClient) => Promise): Promise { diff --git a/backend/src/features/platform/migrations/002_add_catalog_search_index.sql b/backend/src/features/platform/migrations/002_add_catalog_search_index.sql new file mode 100644 index 0000000..2aa03fc --- /dev/null +++ b/backend/src/features/platform/migrations/002_add_catalog_search_index.sql @@ -0,0 +1,14 @@ +-- Migration: Add full-text search index for vehicle catalog search +-- Date: 2025-12-15 + +-- Add full-text search index on vehicle_options table +-- Combines year, make, model, and trim into a single searchable tsvector +-- Using || operator instead of concat() because || is IMMUTABLE +CREATE INDEX IF NOT EXISTS idx_vehicle_options_fts ON vehicle_options +USING gin(to_tsvector('english', year::text || ' ' || make || ' ' || model || ' ' || trim)); + +-- Add an index on engines.name for join performance during search +CREATE INDEX IF NOT EXISTS idx_engines_name ON engines(name); + +-- Add comment for documentation +COMMENT ON INDEX idx_vehicle_options_fts IS 'Full-text search index for admin catalog search functionality'; diff --git a/data/source-makes.txt b/data/source-makes.txt deleted file mode 100644 index 17e70cf..0000000 --- a/data/source-makes.txt +++ /dev/null @@ -1,53 +0,0 @@ -acura -alfa_romeo -aston_martin -audi -bentley -bmw -buick -cadillac -chevrolet -chrysler -dodge -ferrari -fiat -ford -genesis -gmc -honda -hummer -hyundai -infiniti -isuzu -jaguar -jeep -kia -lamborghini -land_rover -lexus -lincoln -lotus -lucid -maserati -mazda -mclaren -mercury -mini -mitsubishi -nissan -oldsmobile -plymouth -polestar -pontiac -porsche -ram -rivian -rolls_royce -saab -scion -smart -subaru -tesla -toyota -volkswagen -volvo \ No newline at end of file diff --git a/data/vehicle-etl/README.md b/data/vehicle-etl/README.md new file mode 100644 index 0000000..92ec49a --- /dev/null +++ b/data/vehicle-etl/README.md @@ -0,0 +1,41 @@ +Step 1: Fetch Data from VehAPI + + cd data/vehicle-etl + python3 vehapi_fetch_snapshot.py --min-year 2015 --max-year 2025 + + Options: + | Flag | Default | Description | + |---------------------|-------------------|------------------------| + | --min-year | 2015 | Start year | + | --max-year | 2022 | End year | + | --rate-per-min | 55 | API rate limit | + | --snapshot-dir | snapshots/ | Output directory | + | --no-response-cache | false | Disable resume caching | + + Output: Creates snapshots//snapshot.sqlite + + --- + Step 2: Generate SQL Files + + python3 etl_generate_sql.py --snapshot-path snapshots//snapshot.sqlite + + Output: Creates output/01_engines.sql, output/02_transmissions.sql, output/03_vehicle_options.sql + + --- + Step 3: Import to PostgreSQL + + ./import_data.sh + + Requires: mvp-postgres container running, SQL files in output/ + + --- + Quick Test (single year) + + python3 vehapi_fetch_snapshot.py --min-year 2020 --max-year 2020 + + # Full ETL workflow + ./reset_database.sh # Clear old data + python3 vehapi_fetch_snapshot.py # Fetch from API + python3 etl_generate_sql.py # Generate SQL + ./import_data.sh # Import to Postgres + docker compose exec mvp-redis redis-cli FLUSHALL # Flush Redis Cache for front end diff --git a/data/vehicle-etl/__pycache__/vehapi_fetch_snapshot.cpython-314.pyc b/data/vehicle-etl/__pycache__/vehapi_fetch_snapshot.cpython-314.pyc index 337e87b9d826c06e981a3c075da26a67c2d7a387..4cf867cb395ca95df5aa60803fe96c97c7ac1ed0 100644 GIT binary patch delta 4314 zcma)93s98T75?wCFW3cE*yXvvq6jQ6u@F%VM#ROSLbeNHBHd0`Fad zM`$KRCX}Oo%|dXxDR>C<$aL4EnI|}-wy52P<+mDw(BRZ4I+SYQvKKV!6(yOC zr!PxOg7v$EtG3jkHCJqT`?WuaxtihFuYJ#$##%d zrHo>JZ2Wkfnp;}1$t;)t07_>p&#^qY;ieYM{DpD)O!;cMi+zxt%8ohWNtiwBc((E} zBshw23}Fc22>>PD>-7a;pFY_i3QArteG;cmA`Bxu&c5wWj5&c#gfM$LyONv$?)juI z5#VxQR2E5m%SpZ*F)oH}(^qb#Acmb}I`lzz;K zMkB6;uaPXiliTVTyHHXnDAHd7Eq#(X$};ub@h-5HW$Vc(J5hF>Twn*vbI2R)#qxVN zuXiJ#l>0TN88-AHYp+<4&Bg0g@fVQY1j1E>_YoBKvx;i+F8j3N68S0n&EiFz*|o@5 zi{H?&@~WiBdrP0tlCw-+{wn!1TTuM~c|WqVI*ZJ^jMQjk#av(gptriI$yM)eYNY3I zoFP2TMprD;AHwDlrmM*#@3Z2X+>(npa1Fp2ud4MY9QY97V+7O^OyYwB?xYiJcTH8S z63y?Qw<|PELao zGCV$`B@v9I@q!(y=0siy{T0BU6MYg$QCWc!-Sg!3$n ztgL@Sv+(x$aw;PY?ckoP+6wQM)2wbQ3E3nQwtPc!t{vMM0E$@*Zt(|2Z-*}!3MwX< z`r!KU4*dg@-8H0;opawP=EQqZxnv~eYNlW-l{4XF0ly@{?WGy4zE$0~JEHX%HDImS zrqntuE>A>o^G#xlJJc{L8Fz=0Fg3%S&c~IgENw)>4_T zRkuR(*>KZ#;*OZt<&d;*R(wXw&5{1*Et-t8$ehc{gjNIGe7;LnZT6oBb1UbfOs?|n zz*Goh&e(5O{+7z^8`5nHP+Q})w(5@3*})9~vN`ha4OPVUwYs93J26biKrTTxfsp=% zVAiEiA&ivWz`rvc{O{NE_}}kx_?(*0Fo#WlMriPS)&RVA32lJthsoHCX;;HB$}D_K z-!_w!-0XWPi58Eh;?_u(;E8H8u<_Q^BrT`YaXMWyrwi+v8If(w))vq9)Fo=0^&UN2 z+Ln=Oo|YCiG#e~JtqtP15fWlN>uD<_7It=iB1vRt+cI_qyatutEAju=zk5)7&?Wk{(zNNnXuA(teOKFMnsR&-Dw!4(QSHHnbf7JbLcK2J~K!kzEs$4bjb7xB*6X zQY_E{_e5~e#B}R4n=D}#aSY5`x$7peu_ZQQVo!+0ra>qgg-opWQutgtxV62|Mb^a6 zOjxJo-~I=$H-7?WAke?De|0V1sgTxzsCGv__w)gc(=niT;MY13=Lq}!RB}imN0*Pb zJL41`1nw>3tlFjy7~OR%l&Eeos6>mw-hfDbV2Ns6!vh*7idh$e*9UgEM!106F3R88 z$78>wgQd9+_LD6R2RGPP&<9_)rViztEIwX5Vtp~=xr}pjt~e_v*hgC$cTQ&H4)>1u z-peRG5`8#o(qbDTLpei9L(*{B@ciMP(}^SNo=+PwpILE6KGQXpRP|=f+p9-cU$H!J z)nc8n6pmX8M^;|8R3M>#NIp4ud~hOb$#~Y1vCPVIC1Xi-m)(sM?v`zM${=Je zykQh#=Nzhgq;A4&A2;u`Pna{OY=Z6H8%cu2dT8*G!7*c+TFjc%;*XbxJR0z`;H4oi z5_#=#8jpt8f6rrU5kP~}Mjyk@QG{a%SjsBam9E+9AxP+lfu z=|(`1N-`4Z+UAHNlWbq$guWHIwJ~R~rwB@D zC9c_Zf3>^Li#3$TT|ban@9V>t9=@w22fXhbunKBP_EvC`w6Rm6KC+J4!-cwSz-J_~ zHr!84bC4wu8k20Sr-Z!B{@XKLj8zW2Soq{5>Wwcv^|g|Ft3Vc!5aDHZfd-&}s+3B| z`G{XSZ*czK6*#LO%+#wjKI<8*v~a6&|5md$J3n}oY-65>D#;FZ@S(J(yTa&Z)U6F+ zF2bh(ilI?dYa4#x++Sv15z3rk2wt927#=QIM`2O^0t;27kVa;Gc;3QUqLJT|2;2`g zVCy2X*LL;>!p=n6gZuZhtq1Fz0SN_%k< z9au4S!IMCOa@FgT=_pekF4FxFCeKGq-~C*(tR9*T4bF0U9M{HpyFBRd2oVIHTo1APc5klbXJV=gghNzzQ3_QsKf}*pnW7c@{4)JMa_K_Yg0K$Z zI&xWptx|-o?Bm@VZGNEY?rL-rA&1}Vgz zOrJsQY?i5*JuB;(F(XDLOKz5yu`)StR1h_JedQxq99_a&?9q&jVk@wA39C$gr01Vq zXcYWptmP201v&>>Hj(atLeV?hymBZ&H?#j{lqSVPSC=r9k^7N8ZD{JWZuOp6_Ivky z)?;26AETK$Dgg8*^MajDoD>st?M~2H3=(K9im<6 z5G!K^VdI=eao%Y`XjKVR6A*!oJ+3XaBn= zHEv#_Ab4QVEL1O8Yk;9Fn~LIgdSFHDS9$%my{GxGG-3rfO*Y&#}=DtJl{ptg-18b;#?Jlo(&Y9|#5f zUN_wZ(iK&^T5}ujcy?ki8{Y1g> zfo!40)RWACFl-z#!{i(jn(~3pS5UW z)pa$v=uQ2Y_fRvAb+NatlUQd~JPEPxtRt%qU?=XF{siG5!gByhywmCOgRNa5Z@@1* zo%ApcJ&*7L!m|iz2%Hyvf%c6-i2cx7O!`1p9@!fH+&W131tQJ4?6WCmvMsF2ep$>1 zClC}Zl|$4Uq{ne>5yHG5Hig7dw*s zHF<)SHCZ)bX!nNCPTQ=m_!U&?>j0>k&*u%<2-i$TpU;i(zk$Q)ckpw18D#lfy=-Ux z-#BT1I9T8zSxBlFq)y4*5rT35X8I2FjJfq$cFkT%`dRVJt9;SfaPB>)NfzH{R2_W_ z?j+!s=owf`4>K~`qUG0OfK8pfikxNJX8)56unlu^$ZuHBoO?Ofhv5(Actyi)8al{I zi)K#d;ax|&vWA9=>biz{dK&xpBOGIg%SyF- zu=*7HtZW*&%;Fa3+6S@Y3VqT6TKaTVbo2&gKYiwmsmp_kbD#U(nr4+lPGFD`z7 zTnS%aY$H0?VE&@>=0)MDOLmE%{Y#b8x%L_1;mS|M;`d>`O{Yxo%3eRj5WicZe@2S0 zkXzjjp`4~-dkO*ruxaT8IDBeg-&Cz3>EV@2KO&rPX4t;`9dTOJwzx#m)4(R~(~)z+ zJ7_b)ZrJ2ntBzWbDT|fXW|0f5xwf3lWQS`<3evE9CrX@zHLgH9)-nJTV~D!^vd=5a zaD`#@TXm5gHH80FrxzEziej~rzts!QVSz@CO&6iiAw>#89dhSjje8H?d|O;ZtBGKK z=+8o^$7yJ9B|gF@JAg($(+s}uiro#!Z+%3Ncwkg z<+k1}VfV^bF%$KsTy{ih!+53H)m6EfQH7)NssL25#okiAD5`MEcq2vCo`o9g$25-Y zGLvmu?ISJWzpgGJDRPNjES0vw87;4jF$tsW@N2Beh3QzYLX1XU28)(?i|z?~TyseiZVhfYzj0Bq z(FcoxSlf78M1{rrTpOgQtowlpJk8r4k|>0yN$mRi2T2dBb-xD>D7-QyVW2t)ljJ0* zjQ6LWHRD_dCfxpFpvGP23&KJkhgkV0`@qvdO4uT4bxxxQ&QJu-pjwOb_;<^(w2k0W z4&7`G8<7fTB#ezl%T-BZi5=u{^CFpJqfnQ~Hs&RsW<0P!GVIK$tN<8Wu8hnZ!5mQ%x(4C zFQsMnsE17{y}CYi@Avyw^o9Cb2J%Y=QcHhb|6bGEP3KK@L#E`5rWxlATCcK+g>#NG15<^_f%mc=(SGSyki{_HW-o?SshO z#18s5=EJX+64zMqKv`Xt6Y~H^UH!wBYFDR~N*g;QIV4+y61BptfZxOJ3k;JcmKy9N z^=xl&hNca8j)&h0ZYD`dNXakgCw$WY?9++4pKKw!EdNH2ix>Ar}PhP#f*Q zp)LfzU35+Esfv!@Y>86Ld->f|3@wo2%8+?ET_JjwRX>uic>)H{gdch2hFDq))tVYx zA$N!KBb7i*{*{Tkslgc}HZ*ej2&ujDo{RQZAd8mgc)KqFxT z1HYn{I=vzK17z_aNC@=^qew6xYqJqrSksRDl0NL@Rz*MJXHQYt$fe^3Ny8~-1pH@E z^a~(_b+k*R^vCSo9Y33CBJi&cFp?{XhMS_ASVjbZYbt1tVDo0&1~q|v0P*@-*1NNq KRI|aI<^KoGkYf=5 diff --git a/data/vehicle-etl/output/01_engines.sql b/data/vehicle-etl/output/01_engines.sql index c631adf..d9bfedc 100644 --- a/data/vehicle-etl/output/01_engines.sql +++ b/data/vehicle-etl/output/01_engines.sql @@ -1,22 +1,128 @@ -- Auto-generated by etl_generate_sql.py INSERT INTO engines (id, name, fuel_type) VALUES (1,'Gas','Gas'), -(2,'2.0L 150 hp I4','Gas'), -(3,'2.4L 201 hp I4','Gas'), -(4,'3.5L 290 hp V6','Gas'), -(5,'3.5L 273 hp V6','Gas'), -(6,'3.5L 310 hp V6','Gas'), -(7,'2.4L 206 hp I4','Gas'), -(8,'2.0L 220 hp I4','Gas'), -(9,'1.8L 170 hp I4','Gas'), -(10,'Diesel','Diesel'), -(11,'2.0L 150 hp I4 Diesel','Diesel'), -(12,'2.0L 220 hp I4 Flex Fuel Vehicle','Gas'), -(13,'3.0L 310 hp V6','Gas'), -(14,'3.0L 240 hp V6 Diesel','Diesel'), -(15,'4.0L 435 hp V8','Diesel'), +(2,'2.4L 201 hp I4','Gas'), +(3,'3.5L 290 hp V6','Gas'), +(4,'3.0L 321 hp V6 Hybrid','Hybrid'), +(5,'3.5L 573 hp V6 Hybrid','Hybrid'), +(6,'3.5L 279 hp V6','Gas'), +(7,'3.5L 310 hp V6','Gas'), +(8,'3.5L 377 hp V6 Hybrid','Hybrid'), +(9,'2.4L 206 hp I4','Gas'), +(10,'2.0L 220 hp I4','Gas'), +(11,'2.0L 186 hp I4','Gas'), +(12,'1.4L 204 hp I4','Gas'), +(13,'2.0L 190 hp I4','Gas'), +(14,'2.0L 252 hp I4','Gas'), +(15,'2.0L 220 hp I4 Flex Fuel Vehicle','Gas'), (16,'3.0L 333 hp V6','Gas'), -(17,'6.3L 500 hp W12','Gas'), -(18,'2.0L 200 hp I4','Gas'), -(19,'3.0L 272 hp V6','Gas'); +(17,'3.0L 340 hp V6','Gas'), +(18,'4.0L 450 hp V8','Gas'), +(19,'2.0L 200 hp I4','Gas'), +(20,'3.0L 272 hp V6','Gas'), +(21,'Diesel','Diesel'), +(22,'5.2L 540 hp V10','Gas'), +(23,'5.2L 610 hp V10','Gas'), +(24,'2.5L 400 hp I5','Gas'), +(25,'4.0L 560 hp V8','Gas'), +(26,'4.0L 605 hp V8','Gas'), +(27,'2.0L 292 hp I4','Gas'), +(28,'3.0L 354 hp V6','Gas'), +(29,'2.0L 248 hp I4','Gas'), +(30,'3.0L 335 hp I6','Gas'), +(31,'2.0L 180 hp I4','Gas'), +(32,'2.0L 180 hp I4 Diesel','Diesel'), +(33,'3.0L 320 hp I6','Gas'), +(34,'3.0L 300 hp I6','Gas'), +(35,'4.4L 445 hp V8','Gas'), +(36,'3.0L 315 hp I6','Gas'), +(37,'4.4L 600 hp V8','Gas'), +(38,'2.0L 322 hp I4','Gas'), +(39,'6.6L 601 hp V12','Gas'), +(40,'3.0L 365 hp I6','Gas'), +(41,'3.0L 425 hp I6','Gas'), +(42,'4.4L 552 hp V8','Gas'), +(43,'2.0L 228 hp I4','Gas'), +(44,'2.0L 240 hp I4','Gas'), +(45,'3.0L 355 hp I6','Gas'), +(46,'3.0L 255 hp I6 Diesel','Diesel'), +(47,'2.0L 308 hp I4','Gas'), +(48,'4.4L 567 hp V8','Gas'), +(49,'0.7L 168 hp I2','Gas'), +(50,'170 hp Electric','Electric'), +(51,'168 hp Electric','Electric'), +(52,'0.7L 170 hp I2','Gas'), +(53,'1.5L 357 hp I3','Gas'), +(54,'6.0L 600 hp W12','Gas'), +(55,'6.0L 633 hp W12','Gas'), +(56,'4.0L 500 hp V8','Gas'), +(57,'4.0L 521 hp V8','Gas'), +(58,'6.0L 582 hp W12','Gas'), +(59,'6.0L 700 hp W12','Gas'), +(60,'6.0L 616 hp W12','Gas'), +(61,'6.0L 626 hp W12','Gas'), +(62,'6.8L 505 hp V8','Gas'), +(63,'6.8L 530 hp V8','Gas'), +(64,'1.6L 200 hp I4','Gas'), +(65,'3.6L 288 hp V6','Gas'), +(66,'1.4L 138 hp I4','Gas'), +(67,'1.4L 153 hp I4','Gas'), +(68,'2.5L 197 hp I4','Gas'), +(69,'3.6L 310 hp V6','Gas'), +(70,'2.4L 182 hp I4','Gas'), +(71,'2.4L 182 hp I4 Flex Fuel Vehicle','Gas'), +(72,'2.0L 259 hp I4','Gas'), +(73,'2.4L 180 hp I4','Gas'), +(74,'2.4L 180 hp I4 Flex Fuel Vehicle','Gas'), +(75,'2.0L 272 hp I4','Gas'), +(76,'3.6L 335 hp V6','Gas'), +(77,'2.5L 202 hp I4','Gas'), +(78,'3.6L 464 hp V6','Gas'), +(79,'2.0L 265 hp I4','Gas'), +(80,'3.0L 404 hp V6','Gas'), +(81,'2.0L 335 hp I4','Gas'), +(82,'2.0L 268 hp I4','Gas'), +(83,'3.6L 420 hp V6','Gas'), +(84,'6.2L 640 hp V8','Gas'), +(85,'6.2L 420 hp V8','Gas'), +(86,'3.6L 304 hp V6','Gas'), +(87,'3.6L 410 hp V6','Gas'), +(88,'200 hp Electric','Electric'), +(89,'2.0L 275 hp I4','Gas'), +(90,'6.2L 455 hp V8','Gas'), +(91,'6.2L 650 hp V8','Gas'), +(92,'3.6L 301 hp V6 Flex Fuel Vehicle','Gas'), +(93,'6.0L 355 hp V8 Flex Fuel Vehicle','Gas'), +(94,'2.0L 131 hp I4','Gas'), +(95,'2.5L 200 hp I4','Gas'), +(96,'2.8L 181 hp I4 Diesel','Diesel'), +(97,'3.6L 308 hp V6','Gas'), +(98,'6.2L 460 hp V8','Gas'), +(99,'1.6L 137 hp I4 Diesel','Diesel'), +(100,'3.6L 301 hp V6','Gas'), +(101,'4.8L 285 hp V8','Gas'), +(102,'6.0L 342 hp V8 Flex Fuel Vehicle','Gas'), +(103,'3.6L 260 hp V6 Compressed Natural Gas','Gas'), +(104,'3.6L 305 hp V6 Flex Fuel Vehicle','Gas'), +(105,'1.5L 160 hp I4','Gas'), +(106,'2.0L 250 hp I4','Gas'), +(107,'1.8L 182 hp I4 Hybrid','Hybrid'), +(108,'6.2L 415 hp V8','Gas'), +(109,'4.3L 285 hp V6 Flex Fuel Vehicle','Gas'), +(110,'5.3L 355 hp V8','Gas'), +(111,'6.0L 360 hp V8 Flex Fuel Vehicle','Gas'), +(112,'6.6L 445 hp V8 Biodiesel','Diesel'), +(113,'1.8L 138 hp I4','Gas'), +(114,'1.4L 98 hp I4','Gas'), +(115,'5.3L 355 hp V8 Flex Fuel Vehicle','Gas'), +(116,'6.0L 360 hp V8','Gas'), +(117,'3.6L 281 hp V6','Gas'), +(118,'1.5L 149 hp I4','Gas'), +(119,'2.4L 184 hp I4','Gas'), +(120,'3.6L 295 hp V6','Gas'), +(121,'3.6L 292 hp V6','Gas'), +(122,'5.7L 363 hp V8','Gas'), +(123,'3.6L 300 hp V6','Gas'), +(124,'3.6L 287 hp V6','Gas'), +(125,'3.6L 260 hp V6','Gas'); diff --git a/data/vehicle-etl/output/02_transmissions.sql b/data/vehicle-etl/output/02_transmissions.sql index 3b302ce..fcc9830 100644 --- a/data/vehicle-etl/output/02_transmissions.sql +++ b/data/vehicle-etl/output/02_transmissions.sql @@ -2,12 +2,19 @@ INSERT INTO transmissions (id, type) VALUES (1,'Automatic'), (2,'Manual'), -(3,'5-Speed Automatic'), -(4,'6-Speed Manual'), -(5,'6-Speed Automatic'), -(6,'8-Speed Dual Clutch'), -(7,'9-Speed Automatic'), +(3,'8-Speed Dual Clutch'), +(4,'9-Speed Automatic'), +(5,'7-Speed Dual Clutch'), +(6,'9-Speed Dual Clutch'), +(7,'6-Speed Automatic'), (8,'6-Speed Dual Clutch'), -(9,'8-Speed Automatic'), -(10,'Continuously Variable Transmission'); +(9,'6-Speed Manual'), +(10,'8-Speed Automatic'), +(11,'1-Speed Dual Clutch'), +(12,'6-Speed Automatic Overdrive'), +(13,'4-Speed Automatic'), +(14,'10-Speed Automatic'), +(15,'Continuously Variable Transmission'), +(16,'7-Speed Manual'), +(17,'5-Speed Manual'); diff --git a/data/vehicle-etl/output/03_vehicle_options.sql b/data/vehicle-etl/output/03_vehicle_options.sql index 77cd81e..e2caf7c 100644 --- a/data/vehicle-etl/output/03_vehicle_options.sql +++ b/data/vehicle-etl/output/03_vehicle_options.sql @@ -1,281 +1,1921 @@ -- Auto-generated by etl_generate_sql.py INSERT INTO vehicle_options (year, make, model, trim, engine_id, transmission_id) VALUES -(2015,'Acura','ILX','2.0L',1,1), -(2015,'Acura','ILX','2.0L',1,2), -(2015,'Acura','ILX','2.0L FWD',2,3), -(2015,'Acura','ILX','2.0L FWD with Premium Package',2,3), -(2015,'Acura','ILX','2.0L FWD with Technology Package',2,3), -(2015,'Acura','ILX','2.0L Technology',1,1), -(2015,'Acura','ILX','2.0L Technology',1,2), -(2015,'Acura','ILX','2.0L w/Premium Package',1,1), -(2015,'Acura','ILX','2.0L w/Premium Package',1,2), -(2015,'Acura','ILX','2.4L FWD with Premium Package',2,3), -(2015,'Acura','ILX','2.4L FWD with Premium Package',3,4), -(2015,'Acura','ILX','2.4L w/Premium Package',1,1), -(2015,'Acura','ILX','2.4L w/Premium Package',1,2), -(2015,'Acura','ILX','FWD with Dynamic Package',2,3), -(2015,'Acura','MDX','3.5L',1,1), -(2015,'Acura','MDX','3.5L',1,2), -(2015,'Acura','MDX','3.5L Advance Pkg w/Entertainment Pkg',1,1), -(2015,'Acura','MDX','3.5L Advance Pkg w/Entertainment Pkg',1,2), -(2015,'Acura','MDX','3.5L Technology Package',1,1), -(2015,'Acura','MDX','3.5L Technology Package',1,2), -(2015,'Acura','MDX','3.5L Technology Pkg/w Entertainment Pkg',1,1), -(2015,'Acura','MDX','3.5L Technology Pkg/w Entertainment Pkg',1,2), -(2015,'Acura','MDX','3.5L w/Technology & Entertainment Pkgs',1,1), -(2015,'Acura','MDX','3.5L w/Technology & Entertainment Pkgs',1,2), -(2015,'Acura','MDX','FWD',4,5), -(2015,'Acura','MDX','FWD with Advance and Entertainment Package',4,5), -(2015,'Acura','MDX','FWD with Technology Package',4,5), -(2015,'Acura','MDX','FWD with Technology and Entertainment Package',4,5), -(2015,'Acura','MDX','SH-AWD',4,5), -(2015,'Acura','MDX','SH-AWD with Advance and Entertainment Package',4,5), -(2015,'Acura','MDX','SH-AWD with Elite Package',4,5), -(2015,'Acura','MDX','SH-AWD with Navigation',4,5), -(2015,'Acura','MDX','SH-AWD with Technology Package',4,5), -(2015,'Acura','MDX','SH-AWD with Technology and Entertainment Package',4,5), -(2015,'Acura','RDX','AWD',5,5), -(2015,'Acura','RDX','AWD with Technology Package',5,5), -(2015,'Acura','RDX','Base',1,1), -(2015,'Acura','RDX','Base',1,2), -(2015,'Acura','RDX','FWD',5,5), -(2015,'Acura','RDX','FWD with Technology Package',5,5), -(2015,'Acura','RDX','Technology Package',1,1), -(2015,'Acura','RDX','Technology Package',1,2), -(2015,'Acura','RLX','Advance Package',1,1), -(2015,'Acura','RLX','Advance Package',1,2), -(2015,'Acura','RLX','Base',1,1), -(2015,'Acura','RLX','Base',1,2), -(2015,'Acura','RLX','FWD',6,5), -(2015,'Acura','RLX','FWD',1,1), -(2015,'Acura','RLX','FWD',1,2), -(2015,'Acura','RLX','FWD with Advance Package',6,5), -(2015,'Acura','RLX','FWD with Elite Package',6,5), -(2015,'Acura','RLX','FWD with Krell Audio Package',6,5), -(2015,'Acura','RLX','FWD with Navigation',6,5), -(2015,'Acura','RLX','FWD with Technology Package',6,5), -(2015,'Acura','RLX','Navigation',1,1), -(2015,'Acura','RLX','Navigation',1,2), -(2015,'Acura','RLX','Technology Package',1,1), -(2015,'Acura','RLX','Technology Package',1,2), -(2015,'Acura','RLX Hybrid Sport','SH-AWD',1,1), -(2015,'Acura','RLX Hybrid Sport','SH-AWD',1,2), -(2015,'Acura','TLX','Base',1,1), -(2015,'Acura','TLX','Base',1,2), -(2015,'Acura','TLX','FWD',7,6), -(2015,'Acura','TLX','FWD with Technology Package',7,6), -(2015,'Acura','TLX','SH-AWD with Elite Package',4,7), -(2015,'Acura','TLX','Tech',1,1), -(2015,'Acura','TLX','Tech',1,2), -(2015,'Acura','TLX','V6',1,1), -(2015,'Acura','TLX','V6',1,2), -(2015,'Acura','TLX','V6 Advance',1,1), -(2015,'Acura','TLX','V6 Advance',1,2), -(2015,'Acura','TLX','V6 FWD',4,7), -(2015,'Acura','TLX','V6 FWD with Advance Package',4,7), -(2015,'Acura','TLX','V6 FWD with Technology Package',4,7), -(2015,'Acura','TLX','V6 SH-AWD',4,7), -(2015,'Acura','TLX','V6 SH-AWD with Advance Package',4,7), -(2015,'Acura','TLX','V6 SH-AWD with Technology Package',4,7), -(2015,'Acura','TLX','V6 Tech',1,1), -(2015,'Acura','TLX','V6 Tech',1,2), -(2015,'Acura','TLX','V6 with Elite Package',4,7), -(2015,'Audi','A3','1.8T Komfort Sedan FWD',8,8), -(2015,'Audi','A3','1.8T Premium',1,1), -(2015,'Audi','A3','1.8T Premium',1,2), -(2015,'Audi','A3','1.8T Premium Cabriolet FWD',9,8), -(2015,'Audi','A3','1.8T Premium Plus',1,1), -(2015,'Audi','A3','1.8T Premium Plus',1,2), -(2015,'Audi','A3','1.8T Premium Plus Cabriolet FWD',9,8), -(2015,'Audi','A3','1.8T Premium Plus Sedan FWD',9,8), -(2015,'Audi','A3','1.8T Premium Sedan FWD',9,8), -(2015,'Audi','A3','1.8T Prestige',1,1), -(2015,'Audi','A3','1.8T Prestige',1,2), -(2015,'Audi','A3','1.8T Prestige Cabriolet FWD',9,8), -(2015,'Audi','A3','1.8T Prestige Sedan FWD',9,8), -(2015,'Audi','A3','1.8T Prestige Sedan FWD',8,8), -(2015,'Audi','A3','1.8T Progressiv Sedan FWD',8,8), -(2015,'Audi','A3','2.0 TDI Premium',10,1), -(2015,'Audi','A3','2.0 TDI Premium',10,2), -(2015,'Audi','A3','2.0 TDI Premium Plus',10,1), -(2015,'Audi','A3','2.0 TDI Premium Plus',10,2), -(2015,'Audi','A3','2.0 TDI Premium Plus Sedan FWD',11,8), -(2015,'Audi','A3','2.0 TDI Premium Sedan FWD',11,8), -(2015,'Audi','A3','2.0 TDI Prestige',10,1), -(2015,'Audi','A3','2.0 TDI Prestige',10,2), -(2015,'Audi','A3','2.0 TDI Prestige Sedan FWD',11,8), -(2015,'Audi','A3','2.0T Premium',1,1), -(2015,'Audi','A3','2.0T Premium',1,2), -(2015,'Audi','A3','2.0T Premium Plus',1,1), -(2015,'Audi','A3','2.0T Premium Plus',1,2), -(2015,'Audi','A3','2.0T Prestige',1,1), -(2015,'Audi','A3','2.0T Prestige',1,2), -(2015,'Audi','A3','2.0T quattro Komfort Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Komfort Sedan AWD',8,8), -(2015,'Audi','A3','2.0T quattro Premium Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Premium Plus Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Premium Plus Sedan AWD',8,8), -(2015,'Audi','A3','2.0T quattro Premium Sedan AWD',8,8), -(2015,'Audi','A3','2.0T quattro Prestige Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Prestige Sedan AWD',8,8), -(2015,'Audi','A3','2.0T quattro Progressiv Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Progressiv Sedan AWD',8,8), -(2015,'Audi','A3','2.0T quattro Technik Cabriolet AWD',8,8), -(2015,'Audi','A3','2.0T quattro Technik FWD',1,1), -(2015,'Audi','A3','2.0T quattro Technik FWD',1,2), -(2015,'Audi','A3','2.0T quattro Technik Sedan AWD',8,8), -(2015,'Audi','A3','TDI Komfort Sedan FWD',8,8), -(2015,'Audi','A3','TDI Progressiv Sedan FWD',8,8), -(2015,'Audi','A3','TDI Technik Sedan FWD',8,8), -(2015,'Audi','A4','2.0T FrontTrak Komfort FWD',8,4), -(2015,'Audi','A4','2.0T FrontTrak Komfort FWD',8,9), -(2015,'Audi','A4','2.0T Premium',1,1), -(2015,'Audi','A4','2.0T Premium',1,2), -(2015,'Audi','A4','2.0T Premium FWD',8,10), -(2015,'Audi','A4','2.0T Premium Plus',1,1), -(2015,'Audi','A4','2.0T Premium Plus',1,2), -(2015,'Audi','A4','2.0T Premium Plus FWD',8,10), -(2015,'Audi','A4','2.0T Premium Plus Sedan FWD',8,10), -(2015,'Audi','A4','2.0T Premium Sedan FWD',8,10), -(2015,'Audi','A4','2.0T Prestige',1,1), -(2015,'Audi','A4','2.0T Prestige',1,2), -(2015,'Audi','A4','2.0T Prestige FWD',8,4), -(2015,'Audi','A4','2.0T Prestige FWD',8,9), -(2015,'Audi','A4','2.0T Prestige Sedan FWD',8,10), -(2015,'Audi','A4','2.0T quattro Komfort AWD',8,4), -(2015,'Audi','A4','2.0T quattro Komfort AWD',8,9), -(2015,'Audi','A4','2.0T quattro Premium AWD',8,4), -(2015,'Audi','A4','2.0T quattro Premium AWD',8,9), -(2015,'Audi','A4','2.0T quattro Premium Plus AWD',8,4), -(2015,'Audi','A4','2.0T quattro Premium Plus AWD',8,9), -(2015,'Audi','A4','2.0T quattro Premium Plus Sedan AWD',8,4), -(2015,'Audi','A4','2.0T quattro Premium Plus Sedan AWD',8,9), -(2015,'Audi','A4','2.0T quattro Premium Sedan AWD',8,4), -(2015,'Audi','A4','2.0T quattro Premium Sedan AWD',8,9), -(2015,'Audi','A4','2.0T quattro Prestige AWD',8,4), -(2015,'Audi','A4','2.0T quattro Prestige AWD',8,9), -(2015,'Audi','A4','2.0T quattro Prestige Sedan AWD',8,4), -(2015,'Audi','A4','2.0T quattro Prestige Sedan AWD',8,9), -(2015,'Audi','A4','2.0T quattro Progressiv AWD',8,4), -(2015,'Audi','A4','2.0T quattro Progressiv AWD',8,9), -(2015,'Audi','A4','2.0T quattro Technik AWD',8,4), -(2015,'Audi','A4','2.0T quattro Technik AWD',8,9), -(2015,'Audi','A4 Allroad','2.0T quattro Komfort AWD',12,9), -(2015,'Audi','A4 Allroad','2.0T quattro Premium AWD',8,9), -(2015,'Audi','A4 Allroad','2.0T quattro Premium AWD',12,9), -(2015,'Audi','A4 Allroad','2.0T quattro Premium Plus AWD',8,9), -(2015,'Audi','A4 Allroad','2.0T quattro Premium Plus AWD',12,9), -(2015,'Audi','A4 Allroad','2.0T quattro Prestige AWD',8,9), -(2015,'Audi','A4 Allroad','2.0T quattro Prestige AWD',12,9), -(2015,'Audi','A4 Allroad','2.0T quattro Progressiv AWD',12,9), -(2015,'Audi','A4 Allroad','2.0T quattro Technik AWD',12,9), -(2015,'Audi','A5','2.0T Premium',1,1), -(2015,'Audi','A5','2.0T Premium',1,2), -(2015,'Audi','A5','2.0T Premium Plus',1,1), -(2015,'Audi','A5','2.0T Premium Plus',1,2), -(2015,'Audi','A5','2.0T quattro Komfort Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Komfort Coupe AWD',8,9), -(2015,'Audi','A5','2.0T quattro Premium Cabriolet AWD',8,9), -(2015,'Audi','A5','2.0T quattro Premium Cabriolet AWD',12,9), -(2015,'Audi','A5','2.0T quattro Premium Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Premium Coupe AWD',8,9), -(2015,'Audi','A5','2.0T quattro Premium Plus Cabriolet AWD',8,9), -(2015,'Audi','A5','2.0T quattro Premium Plus Cabriolet AWD',12,9), -(2015,'Audi','A5','2.0T quattro Premium Plus Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Premium Plus Coupe AWD',8,9), -(2015,'Audi','A5','2.0T quattro Prestige Cabriolet AWD',8,4), -(2015,'Audi','A5','2.0T quattro Prestige Cabriolet AWD',8,9), -(2015,'Audi','A5','2.0T quattro Prestige Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Prestige Coupe AWD',8,9), -(2015,'Audi','A5','2.0T quattro Progressiv Cabriolet AWD',8,4), -(2015,'Audi','A5','2.0T quattro Progressiv Cabriolet AWD',8,9), -(2015,'Audi','A5','2.0T quattro Progressiv Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Progressiv Coupe AWD',8,9), -(2015,'Audi','A5','2.0T quattro Progressiv Coupe AWD',1,1), -(2015,'Audi','A5','2.0T quattro Progressiv Coupe AWD',1,2), -(2015,'Audi','A5','2.0T quattro Technik Cabriolet AWD',8,4), -(2015,'Audi','A5','2.0T quattro Technik Cabriolet AWD',8,9), -(2015,'Audi','A5','2.0T quattro Technik Coupe AWD',8,4), -(2015,'Audi','A5','2.0T quattro Technik Coupe AWD',8,9), -(2015,'Audi','A6','2.0T Premium',1,1), -(2015,'Audi','A6','2.0T Premium',1,2), -(2015,'Audi','A6','2.0T Premium Plus Sedan FWD',8,10), -(2015,'Audi','A6','2.0T Premium Sedan FWD',8,10), -(2015,'Audi','A6','2.0T Premium Sedan FWD',13,9), -(2015,'Audi','A6','2.0T quattro Premium Plus Sedan AWD',8,9), -(2015,'Audi','A6','2.0T quattro Premium Sedan AWD',8,9), -(2015,'Audi','A6','2.0T quattro Progressiv Sedan AWD',13,9), -(2015,'Audi','A6','2.0T quattro Technik Sedan AWD',13,9), -(2015,'Audi','A6','3.0 TDI Premium Plus',10,1), -(2015,'Audi','A6','3.0 TDI Premium Plus',10,2), -(2015,'Audi','A6','3.0 TDI quattro Premium Plus Sedan AWD',14,9), -(2015,'Audi','A6','3.0 TDI quattro Prestige Sedan AWD',14,9), -(2015,'Audi','A6','3.0 TDI quattro Progressiv Sedan AWD',13,9), -(2015,'Audi','A6','3.0 TDI quattro Technik Sedan AWD',13,9), -(2015,'Audi','A6','3.0T Premium Plus',1,1), -(2015,'Audi','A6','3.0T Premium Plus',1,2), -(2015,'Audi','A6','3.0T Prestige',1,1), -(2015,'Audi','A6','3.0T Prestige',1,2), -(2015,'Audi','A6','3.0T quattro Premium Plus Sedan AWD',13,9), -(2015,'Audi','A6','3.0T quattro Prestige Sedan AWD',13,9), -(2015,'Audi','A6','3.0T quattro Progressiv Sedan AWD',13,9), -(2015,'Audi','A6','3.0T quattro Technik Sedan AWD',13,9), -(2015,'Audi','A7','3.0 TDI Premium Plus',10,1), -(2015,'Audi','A7','3.0 TDI Premium Plus',10,2), -(2015,'Audi','A7','3.0 TDI quattro Premium Plus AWD',14,9), -(2015,'Audi','A7','3.0 TDI quattro Premium Plus AWD',13,9), -(2015,'Audi','A7','3.0 TDI quattro Prestige AWD',14,9), -(2015,'Audi','A7','3.0 TDI quattro Progressiv AWD',13,9), -(2015,'Audi','A7','3.0 TDI quattro Technik AWD',13,9), -(2015,'Audi','A7','3.0T Premium Plus',1,1), -(2015,'Audi','A7','3.0T Premium Plus',1,2), -(2015,'Audi','A7','3.0T Prestige',1,1), -(2015,'Audi','A7','3.0T Prestige',1,2), -(2015,'Audi','A7','3.0T quattro Premium Plus AWD',13,9), -(2015,'Audi','A7','3.0T quattro Prestige AWD',13,9), -(2015,'Audi','A7','3.0T quattro Progressiv AWD',13,9), -(2015,'Audi','A7','3.0T quattro Technik AWD',13,9), -(2015,'Audi','A8','3.0 TDI quattro AWD',15,9), -(2015,'Audi','A8','3.0T',1,1), -(2015,'Audi','A8','3.0T',1,2), -(2015,'Audi','A8','3.0T quattro AWD',16,9), -(2015,'Audi','A8','4.0T',1,1), -(2015,'Audi','A8','4.0T',1,2), -(2015,'Audi','A8','4.0T quattro AWD',15,9), -(2015,'Audi','A8','L 3.0 TDI',10,1), -(2015,'Audi','A8','L 3.0 TDI',10,2), -(2015,'Audi','A8','L 3.0 TDI quattro AWD',14,9), -(2015,'Audi','A8','L 3.0T',1,1), -(2015,'Audi','A8','L 3.0T',1,2), -(2015,'Audi','A8','L 3.0T quattro AWD',16,9), -(2015,'Audi','A8','L 4.0T',1,1), -(2015,'Audi','A8','L 4.0T',1,2), -(2015,'Audi','A8','L 4.0T quattro AWD',15,9), -(2015,'Audi','A8','L W12 6.3',1,1), -(2015,'Audi','A8','L W12 6.3',1,2), -(2015,'Audi','A8','L W12 quattro AWD',15,9), -(2015,'Audi','A8','L W12 quattro AWD',17,9), -(2015,'Audi','Q3','2.0T Premium Plus',1,1), -(2015,'Audi','Q3','2.0T Premium Plus',1,2), -(2015,'Audi','Q3','2.0T Premium Plus FWD',18,5), -(2015,'Audi','Q3','2.0T Prestige',1,1), -(2015,'Audi','Q3','2.0T Prestige',1,2), -(2015,'Audi','Q3','2.0T Prestige FWD',18,5), -(2015,'Audi','Q3','2.0T Progressiv FWD',18,5), -(2015,'Audi','Q3','2.0T Technik FWD',18,5), -(2015,'Audi','Q3','2.0T quattro Premium Plus AWD',18,5), -(2015,'Audi','Q3','2.0T quattro Prestige AWD',18,5), -(2015,'Audi','Q3','3.0T quattro Progressiv AWD',18,5), -(2015,'Audi','Q3','3.0T quattro Technik AWD',18,5), -(2015,'Audi','Q5','2.0T Premium',1,1), -(2015,'Audi','Q5','2.0T Premium',1,2), -(2015,'Audi','Q5','2.0T Premium Plus',1,1), -(2015,'Audi','Q5','2.0T Premium Plus',1,2), -(2015,'Audi','Q5','2.0T quattro Komfort AWD',19,9), -(2015,'Audi','allroad','2.0T Premium',1,1), -(2015,'Audi','allroad','2.0T Premium',1,2), -(2015,'Audi','allroad','2.0T Premium Plus',1,1), -(2015,'Audi','allroad','2.0T Premium Plus',1,2), -(2015,'Audi','allroad','2.0T Prestige',1,1), -(2015,'Audi','allroad','2.0T Prestige',1,2); +(2017,'Acura','ILX','2.4L',1,1), +(2017,'Acura','ILX','2.4L',1,2), +(2017,'Acura','ILX','AcuraWatch Plus Package',1,1), +(2017,'Acura','ILX','AcuraWatch Plus Package',1,2), +(2017,'Acura','ILX','Base',1,1), +(2017,'Acura','ILX','Base',1,2), +(2017,'Acura','ILX','FWD',2,3), +(2017,'Acura','ILX','FWD with A-Spec Package',2,3), +(2017,'Acura','ILX','FWD with AcuraWatch Plus Package',2,3), +(2017,'Acura','ILX','FWD with Premium Package',2,3), +(2017,'Acura','ILX','FWD with Premium and A-Spec Package',2,3), +(2017,'Acura','ILX','FWD with Technology Plus Package',2,3), +(2017,'Acura','ILX','FWD with Technology Plus and A-Spec Package',2,3), +(2017,'Acura','ILX','Premium & A-SPEC Packages',1,1), +(2017,'Acura','ILX','Premium & A-SPEC Packages',1,2), +(2017,'Acura','ILX','Premium Package',1,1), +(2017,'Acura','ILX','Premium Package',1,2), +(2017,'Acura','ILX','Technology Plus & A-SPEC Packages',1,1), +(2017,'Acura','ILX','Technology Plus & A-SPEC Packages',1,2), +(2017,'Acura','ILX','Technology Plus Package',1,1), +(2017,'Acura','ILX','Technology Plus Package',1,2), +(2017,'Acura','MDX','3.5L',1,1), +(2017,'Acura','MDX','3.5L',1,2), +(2017,'Acura','MDX','3.5L w/Advance & Entertainment Pkgs',1,1), +(2017,'Acura','MDX','3.5L w/Advance & Entertainment Pkgs',1,2), +(2017,'Acura','MDX','3.5L w/Advance Package',1,1), +(2017,'Acura','MDX','3.5L w/Advance Package',1,2), +(2017,'Acura','MDX','3.5L w/Technology & Entertainment Pkgs',1,1), +(2017,'Acura','MDX','3.5L w/Technology & Entertainment Pkgs',1,2), +(2017,'Acura','MDX','3.5L w/Technology Package',1,1), +(2017,'Acura','MDX','3.5L w/Technology Package',1,2), +(2017,'Acura','MDX','FWD',3,4), +(2017,'Acura','MDX','FWD with Advance Package',3,4), +(2017,'Acura','MDX','FWD with Advance and Entertainment Package',3,4), +(2017,'Acura','MDX','FWD with Technology and Entertainment Package',3,4), +(2017,'Acura','MDX','FWD wth Technology Package',3,4), +(2017,'Acura','MDX','SH-AWD',3,4), +(2017,'Acura','MDX','SH-AWD with Advance Package',3,4), +(2017,'Acura','MDX','SH-AWD with Advance and Entertainment Package',3,4), +(2017,'Acura','MDX','SH-AWD with Elite 6-Passenger Package',3,4), +(2017,'Acura','MDX','SH-AWD with Elite Package',3,4), +(2017,'Acura','MDX','SH-AWD with Navigation',3,4), +(2017,'Acura','MDX','SH-AWD with Technology Package',3,4), +(2017,'Acura','MDX','SH-AWD with Technology and Entertainment Package',3,4), +(2017,'Acura','MDX Hybrid Sport','SH-AWD with Advance Package',4,5), +(2017,'Acura','MDX Hybrid Sport','SH-AWD with Technology Package',4,5), +(2017,'Acura','MDX Sport Hybrid','3.0L w/Advance Package',1,1), +(2017,'Acura','MDX Sport Hybrid','3.0L w/Advance Package',1,2), +(2017,'Acura','MDX Sport Hybrid','3.0L w/Technology Package',1,1), +(2017,'Acura','MDX Sport Hybrid','3.0L w/Technology Package',1,2), +(2017,'Acura','MDX Sport Hybrid','SH-AWD',4,5), +(2017,'Acura','MDX Sport Hybrid','SH-AWD with Advance Package',4,5), +(2017,'Acura','MDX Sport Hybrid','SH-AWD with Technology Package',4,5), +(2017,'Acura','NSX','Base',1,1), +(2017,'Acura','NSX','Base',1,2), +(2017,'Acura','NSX','SH-AWD',5,6), +(2017,'Acura','RDX','AWD',6,7), +(2017,'Acura','RDX','AWD with AcuraWatch Plus Package',6,7), +(2017,'Acura','RDX','AWD with Advance Package',6,7), +(2017,'Acura','RDX','AWD with Elite Package',6,7), +(2017,'Acura','RDX','AWD with Technology Package',6,7), +(2017,'Acura','RDX','AWD with Technology and AcuraWatch Plus Package',6,7), +(2017,'Acura','RDX','AcuraWatch Plus Package',1,1), +(2017,'Acura','RDX','AcuraWatch Plus Package',1,2), +(2017,'Acura','RDX','Advance Package',1,1), +(2017,'Acura','RDX','Advance Package',1,2), +(2017,'Acura','RDX','Base',1,1), +(2017,'Acura','RDX','Base',1,2), +(2017,'Acura','RDX','FWD',6,7), +(2017,'Acura','RDX','FWD with AcuraWatch Plus Package',6,7), +(2017,'Acura','RDX','FWD with Advance Package',6,7), +(2017,'Acura','RDX','FWD with Technology Package',6,7), +(2017,'Acura','RDX','FWD with Technology and AcuraWatch Plus Package',6,7), +(2017,'Acura','RDX','Technology & AcuraWatch Plus Packages',1,1), +(2017,'Acura','RDX','Technology & AcuraWatch Plus Packages',1,2), +(2017,'Acura','RDX','Technology Package',1,1), +(2017,'Acura','RDX','Technology Package',1,2), +(2017,'Acura','RLX','Advance Package',1,1), +(2017,'Acura','RLX','Advance Package',1,2), +(2017,'Acura','RLX','FWD with Advance Package',7,7), +(2017,'Acura','RLX','FWD with Technology Package',7,7), +(2017,'Acura','RLX','Technology Package',1,1), +(2017,'Acura','RLX','Technology Package',1,2), +(2017,'Acura','RLX Hybrid Sport','SH-AWD Technology Package',8,5), +(2017,'Acura','RLX Hybrid Sport','SH-AWD with Advance Package',8,5), +(2017,'Acura','RLX Hybrid Sport','SH-AWD with Elite Package',1,1), +(2017,'Acura','RLX Hybrid Sport','SH-AWD with Elite Package',1,2), +(2017,'Acura','RLX Sport Hybrid','Advance Package',1,1), +(2017,'Acura','RLX Sport Hybrid','Advance Package',1,2), +(2017,'Acura','RLX Sport Hybrid','Technology Package',1,1), +(2017,'Acura','RLX Sport Hybrid','Technology Package',1,2), +(2017,'Acura','TLX','Base',1,1), +(2017,'Acura','TLX','Base',1,2), +(2017,'Acura','TLX','FWD',9,3), +(2017,'Acura','TLX','FWD with Technology Package',9,3), +(2017,'Acura','TLX','SH-AWD with Elite Package',3,4), +(2017,'Acura','TLX','V6',1,1), +(2017,'Acura','TLX','V6',1,2), +(2017,'Acura','TLX','V6 FWD',3,4), +(2017,'Acura','TLX','V6 FWD with Advance Package',3,4), +(2017,'Acura','TLX','V6 FWD with Technology Package',3,4), +(2017,'Acura','TLX','V6 SH-AWD',3,4), +(2017,'Acura','TLX','V6 SH-AWD with Advance Package',3,4), +(2017,'Acura','TLX','V6 SH-AWD with Technology Package',3,4), +(2017,'Acura','TLX','V6 w/Advance Package',1,1), +(2017,'Acura','TLX','V6 w/Advance Package',1,2), +(2017,'Acura','TLX','V6 w/Technology Package',1,1), +(2017,'Acura','TLX','V6 w/Technology Package',1,2), +(2017,'Acura','TLX','w/Technology Package',1,1), +(2017,'Acura','TLX','w/Technology Package',1,2), +(2017,'Audi','A3','1.8T Premium',1,1), +(2017,'Audi','A3','1.8T Premium',1,2), +(2017,'Audi','A3','2.0T Komfort Sedan FWD',10,8), +(2017,'Audi','A3','2.0T Premium',1,1), +(2017,'Audi','A3','2.0T Premium',1,2), +(2017,'Audi','A3','2.0T Premium Cabriolet FWD',11,5), +(2017,'Audi','A3','2.0T Premium Plus',1,1), +(2017,'Audi','A3','2.0T Premium Plus',1,2), +(2017,'Audi','A3','2.0T Premium Plus Cabriolet FWD',11,5), +(2017,'Audi','A3','2.0T Premium Plus Sedan FWD',11,5), +(2017,'Audi','A3','2.0T Premium Sedan FWD',11,5), +(2017,'Audi','A3','2.0T Prestige Cabriolet FWD',11,5), +(2017,'Audi','A3','2.0T Prestige Cabriolet FWD',10,8), +(2017,'Audi','A3','2.0T Prestige Sedan FWD',11,5), +(2017,'Audi','A3','2.0T Progressiv Sedan FWD',10,8), +(2017,'Audi','A3','2.0T quattro Komfort Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Komfort Sedan AWD',10,8), +(2017,'Audi','A3','2.0T quattro Premium Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Premium Plus Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Premium Plus Sedan AWD',10,8), +(2017,'Audi','A3','2.0T quattro Premium Sedan AWD',10,8), +(2017,'Audi','A3','2.0T quattro Prestige Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Prestige Sedan AWD',10,8), +(2017,'Audi','A3','2.0T quattro Progressiv Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Progressiv Sedan AWD',10,8), +(2017,'Audi','A3','2.0T quattro Technik Cabriolet AWD',10,8), +(2017,'Audi','A3','2.0T quattro Technik Sedan AWD',10,8), +(2017,'Audi','A3 Sportback','e-tron 1.4T Premium FWD',12,8), +(2017,'Audi','A3 Sportback','e-tron 1.4T Premium Plus FWD',12,8), +(2017,'Audi','A3 Sportback','e-tron 1.4T Prestige FWD',12,8), +(2017,'Audi','A3 Sportback','e-tron 1.4T Progressiv FWD',12,8), +(2017,'Audi','A3 Sportback','e-tron 1.4T Technik FWD',12,8), +(2017,'Audi','A3 e-tron','1.4T Premium',1,1), +(2017,'Audi','A3 e-tron','1.4T Premium',1,2), +(2017,'Audi','A4','2.0T Komfort FWD',13,5), +(2017,'Audi','A4','2.0T Premium',1,1), +(2017,'Audi','A4','2.0T Premium',1,2), +(2017,'Audi','A4','2.0T Premium FWD',14,5), +(2017,'Audi','A4','2.0T Premium Plus',1,1), +(2017,'Audi','A4','2.0T Premium Plus',1,2), +(2017,'Audi','A4','2.0T Premium Plus FWD',14,5), +(2017,'Audi','A4','2.0T Premium Plus Sedan FWD',14,5), +(2017,'Audi','A4','2.0T Premium Sedan FWD',14,5), +(2017,'Audi','A4','2.0T Prestige',1,1), +(2017,'Audi','A4','2.0T Prestige',1,2), +(2017,'Audi','A4','2.0T Prestige FWD',14,5), +(2017,'Audi','A4','2.0T Prestige Sedan FWD',14,5), +(2017,'Audi','A4','2.0T Progressiv FWD',13,5), +(2017,'Audi','A4','2.0T quattro Komfort AWD',13,5), +(2017,'Audi','A4','2.0T quattro Premium AWD',14,9), +(2017,'Audi','A4','2.0T quattro Premium AWD',14,5), +(2017,'Audi','A4','2.0T quattro Premium Plus AWD',14,9), +(2017,'Audi','A4','2.0T quattro Premium Plus AWD',14,5), +(2017,'Audi','A4','2.0T quattro Premium Plus Sedan AWD',14,9), +(2017,'Audi','A4','2.0T quattro Premium Plus Sedan AWD',14,5), +(2017,'Audi','A4','2.0T quattro Premium Sedan AWD',14,9), +(2017,'Audi','A4','2.0T quattro Premium Sedan AWD',14,5), +(2017,'Audi','A4','2.0T quattro Prestige AWD',14,9), +(2017,'Audi','A4','2.0T quattro Prestige AWD',14,5), +(2017,'Audi','A4','2.0T quattro Prestige Sedan AWD',14,9), +(2017,'Audi','A4','2.0T quattro Prestige Sedan AWD',14,5), +(2017,'Audi','A4','2.0T quattro Progressiv AWD',13,5), +(2017,'Audi','A4','2.0T quattro Technik Sedan AWD',1,1), +(2017,'Audi','A4','2.0T quattro Technik Sedan AWD',1,2), +(2017,'Audi','A4','2.0T ultra Premium',1,1), +(2017,'Audi','A4','2.0T ultra Premium',1,2), +(2017,'Audi','A4','2.0T ultra Premium FWD',13,5), +(2017,'Audi','A4','2.0T ultra Premium Plus FWD',13,5), +(2017,'Audi','A4','2.0T ultra Premium Plus Sedan FWD',13,5), +(2017,'Audi','A4','2.0T ultra Premium Sedan FWD',13,5), +(2017,'Audi','A4','Season of Audi Premium',1,1), +(2017,'Audi','A4','Season of Audi Premium',1,2), +(2017,'Audi','A4','Season of Audi ultra Premium',1,1), +(2017,'Audi','A4','Season of Audi ultra Premium',1,2), +(2017,'Audi','A4','ultra Premium',1,1), +(2017,'Audi','A4','ultra Premium',1,2), +(2017,'Audi','A4 allroad','2.0T Premium',1,1), +(2017,'Audi','A4 allroad','2.0T Premium',1,2), +(2017,'Audi','A4 allroad','2.0T quattro Komfort AWD',14,5), +(2017,'Audi','A4 allroad','2.0T quattro Premium AWD',14,5), +(2017,'Audi','A4 allroad','2.0T quattro Premium Plus AWD',14,5), +(2017,'Audi','A4 allroad','2.0T quattro Prestige AWD',14,5), +(2017,'Audi','A4 allroad','2.0T quattro Progressiv AWD',14,5), +(2017,'Audi','A4 allroad','2.0T quattro Technik AWD',14,5), +(2017,'Audi','A5','2.0T Sport',1,1), +(2017,'Audi','A5','2.0T Sport',1,2), +(2017,'Audi','A5','2.0T quattro Komfort Coupe AWD',10,9), +(2017,'Audi','A5','2.0T quattro Komfort Coupe AWD',10,10), +(2017,'Audi','A5','2.0T quattro Progressiv Cabriolet AWD',10,9), +(2017,'Audi','A5','2.0T quattro Progressiv Cabriolet AWD',10,10), +(2017,'Audi','A5','2.0T quattro Progressiv Coupe AWD',10,9), +(2017,'Audi','A5','2.0T quattro Progressiv Coupe AWD',10,10), +(2017,'Audi','A5','2.0T quattro Sport Cabriolet AWD',10,10), +(2017,'Audi','A5','2.0T quattro Sport Cabriolet AWD',15,10), +(2017,'Audi','A5','2.0T quattro Sport Coupe AWD',10,9), +(2017,'Audi','A5','2.0T quattro Sport Coupe AWD',10,10), +(2017,'Audi','A5','2.0T quattro Technik Convertible AWD',10,9), +(2017,'Audi','A5','2.0T quattro Technik Convertible AWD',10,10), +(2017,'Audi','A5','2.0T quattro Technik Coupe AWD',10,9), +(2017,'Audi','A5','2.0T quattro Technik Coupe AWD',10,10), +(2017,'Audi','A6','2.0T Premium',1,1), +(2017,'Audi','A6','2.0T Premium',1,2), +(2017,'Audi','A6','2.0T Premium Plus',1,1), +(2017,'Audi','A6','2.0T Premium Plus',1,2), +(2017,'Audi','A6','2.0T Premium Plus Sedan FWD',14,5), +(2017,'Audi','A6','2.0T Premium Sedan FWD',14,5), +(2017,'Audi','A6','2.0T quattro Premium Plus Sedan AWD',14,10), +(2017,'Audi','A6','2.0T quattro Premium Sedan AWD',14,10), +(2017,'Audi','A6','2.0T quattro Technik Sedan AWD',16,10), +(2017,'Audi','A6','3.0T Competition Prestige',1,1), +(2017,'Audi','A6','3.0T Competition Prestige',1,2), +(2017,'Audi','A6','3.0T Premium Plus',1,1), +(2017,'Audi','A6','3.0T Premium Plus',1,2), +(2017,'Audi','A6','3.0T Prestige',1,1), +(2017,'Audi','A6','3.0T Prestige',1,2), +(2017,'Audi','A6','3.0T quattro Competition Prestige Sedan AWD',17,10), +(2017,'Audi','A6','3.0T quattro Premium Plus Sedan AWD',16,10), +(2017,'Audi','A6','3.0T quattro Prestige Sedan AWD',16,10), +(2017,'Audi','A6','3.0T quattro Progressiv Sedan AWD',16,10), +(2017,'Audi','A6','3.0T quattro Technik Sedan AWD',16,10), +(2017,'Audi','A7','3.0T Competition Prestige',1,1), +(2017,'Audi','A7','3.0T Competition Prestige',1,2), +(2017,'Audi','A7','3.0T Premium Plus',1,1), +(2017,'Audi','A7','3.0T Premium Plus',1,2), +(2017,'Audi','A7','3.0T Prestige',1,1), +(2017,'Audi','A7','3.0T Prestige',1,2), +(2017,'Audi','A7','3.0T quattro Competition Prestige AWD',17,10), +(2017,'Audi','A7','3.0T quattro Premium Plus AWD',16,10), +(2017,'Audi','A7','3.0T quattro Prestige AWD',16,10), +(2017,'Audi','A7','3.0T quattro Progressiv AWD',16,10), +(2017,'Audi','A7','3.0T quattro Technik AWD',16,10), +(2017,'Audi','A8','3.0T quattro AWD',18,10), +(2017,'Audi','A8','4.0T quattro AWD',18,10), +(2017,'Audi','A8','L 3.0T',1,1), +(2017,'Audi','A8','L 3.0T',1,2), +(2017,'Audi','A8','L 3.0T quattro AWD',16,10), +(2017,'Audi','A8','L 4.0T Sport',1,1), +(2017,'Audi','A8','L 4.0T Sport',1,2), +(2017,'Audi','A8','L 4.0T quattro AWD',18,10), +(2017,'Audi','A8','L 4.0T quattro Sport AWD',18,10), +(2017,'Audi','Q3','2.0T Komfort FWD',19,7), +(2017,'Audi','Q3','2.0T Premium',1,1), +(2017,'Audi','Q3','2.0T Premium',1,2), +(2017,'Audi','Q3','2.0T Premium FWD',19,7), +(2017,'Audi','Q3','2.0T Premium Plus',1,1), +(2017,'Audi','Q3','2.0T Premium Plus',1,2), +(2017,'Audi','Q3','2.0T Premium Plus FWD',19,7), +(2017,'Audi','Q3','2.0T Prestige',1,1), +(2017,'Audi','Q3','2.0T Prestige',1,2), +(2017,'Audi','Q3','2.0T Prestige FWD',19,7), +(2017,'Audi','Q3','2.0T Progressiv FWD',19,7), +(2017,'Audi','Q3','2.0T Technik FWD',19,7), +(2017,'Audi','Q3','2.0T quattro Komfort AWD',19,7), +(2017,'Audi','Q3','2.0T quattro Premium AWD',19,7), +(2017,'Audi','Q3','2.0T quattro Premium Plus AWD',19,7), +(2017,'Audi','Q3','2.0T quattro Prestige AWD',19,7), +(2017,'Audi','Q3','2.0T quattro Progressiv AWD',19,7), +(2017,'Audi','Q3','2.0T quattro Technik AWD',19,7), +(2017,'Audi','Q5','2.0T Premium',1,1), +(2017,'Audi','Q5','2.0T Premium',1,2), +(2017,'Audi','Q5','2.0T Premium Plus',1,1), +(2017,'Audi','Q5','2.0T Premium Plus',1,2), +(2017,'Audi','Q5','2.0T quattro Komfort AWD',20,10), +(2017,'Audi','Q5','2.0T quattro Premium AWD',10,10), +(2017,'Audi','Q5','2.0T quattro Premium AWD',15,10), +(2017,'Audi','Q5','2.0T quattro Premium Plus AWD',10,10), +(2017,'Audi','Q5','2.0T quattro Premium Plus AWD',15,10), +(2017,'Audi','Q5','2.0T quattro Progressiv AWD',20,10), +(2017,'Audi','Q5','2.0T quattro Technik AWD',20,10), +(2017,'Audi','Q5','3.0T Premium Plus',1,1), +(2017,'Audi','Q5','3.0T Premium Plus',1,2), +(2017,'Audi','Q5','3.0T Prestige',1,1), +(2017,'Audi','Q5','3.0T Prestige',1,2), +(2017,'Audi','Q5','3.0T quattro Premium Plus AWD',20,10), +(2017,'Audi','Q5','3.0T quattro Prestige AWD',20,10), +(2017,'Audi','Q5','3.0T quattro Progressiv AWD',20,10), +(2017,'Audi','Q5','3.0T quattro Technik AWD',20,10), +(2017,'Audi','Q7','2.0T Premium',1,1), +(2017,'Audi','Q7','2.0T Premium',1,2), +(2017,'Audi','Q7','2.0T Premium Plus',1,1), +(2017,'Audi','Q7','2.0T Premium Plus',1,2), +(2017,'Audi','Q7','2.0T quattro Komfort AWD',16,10), +(2017,'Audi','Q7','2.0T quattro Premium AWD',14,10), +(2017,'Audi','Q7','2.0T quattro Premium Plus AWD',14,10), +(2017,'Audi','Q7','2.0T quattro Progressiv AWD',16,10), +(2017,'Audi','Q7','3.0 TDI Premium Plus',21,1), +(2017,'Audi','Q7','3.0 TDI Premium Plus',21,2), +(2017,'Audi','Q7','3.0T Premium',1,1), +(2017,'Audi','Q7','3.0T Premium',1,2), +(2017,'Audi','Q7','3.0T Premium Plus',1,1), +(2017,'Audi','Q7','3.0T Premium Plus',1,2), +(2017,'Audi','Q7','3.0T Prestige',1,1), +(2017,'Audi','Q7','3.0T Prestige',1,2), +(2017,'Audi','Q7','3.0T quattro Komfort AWD',16,10), +(2017,'Audi','Q7','3.0T quattro Premium AWD',16,10), +(2017,'Audi','Q7','3.0T quattro Premium Plus AWD',16,10), +(2017,'Audi','Q7','3.0T quattro Prestige AWD',16,10), +(2017,'Audi','Q7','3.0T quattro Progressiv AWD',16,10), +(2017,'Audi','Q7','3.0T quattro Technik AWD',16,10), +(2017,'Audi','R8','5.2',1,1), +(2017,'Audi','R8','5.2',1,2), +(2017,'Audi','R8','5.2 V10',1,1), +(2017,'Audi','R8','5.2 V10',1,2), +(2017,'Audi','R8','5.2 V10 plus',1,1), +(2017,'Audi','R8','5.2 V10 plus',1,2), +(2017,'Audi','R8','5.2 plus',1,1), +(2017,'Audi','R8','5.2 plus',1,2), +(2017,'Audi','R8','quattro V10 Coupe AWD',22,5), +(2017,'Audi','R8','quattro V10 Plus Coupe AWD',23,5), +(2017,'Audi','R8','quattro V10 Spyder AWD',22,5), +(2017,'Audi','RS 3','2.5T',1,1), +(2017,'Audi','RS 3','2.5T',1,2), +(2017,'Audi','RS 3','2.5T quattro AWD',24,5), +(2017,'Audi','RS 7','4.0T Performance Prestige',1,1), +(2017,'Audi','RS 7','4.0T Performance Prestige',1,2), +(2017,'Audi','RS 7','4.0T Prestige',1,1), +(2017,'Audi','RS 7','4.0T Prestige',1,2), +(2017,'Audi','RS 7','4.0T quattro AWD',25,10), +(2017,'Audi','RS 7','4.0T quattro Performance AWD',26,10), +(2017,'Audi','RS 7','4.0T quattro Performance Prestige AWD',26,10), +(2017,'Audi','S3','2.0T Premium Plus',1,1), +(2017,'Audi','S3','2.0T Premium Plus',1,2), +(2017,'Audi','S3','2.0T Prestige',1,1), +(2017,'Audi','S3','2.0T Prestige',1,2), +(2017,'Audi','S3','2.0T quattro Premium Plus AWD',27,8), +(2017,'Audi','S3','2.0T quattro Prestige AWD',27,8), +(2017,'Audi','S3','2.0T quattro Progressiv AWD',27,8), +(2017,'Audi','S3','2.0T quattro Technik AWD',27,8), +(2017,'Audi','S5','3.0T',1,1), +(2017,'Audi','S5','3.0T',1,2), +(2017,'Audi','S5','3.0T Premium Plus',1,1), +(2017,'Audi','S5','3.0T Premium Plus',1,2), +(2017,'Audi','S5','3.0T quattro Cabriolet AWD',16,5), +(2017,'Audi','S5','3.0T quattro Coupe AWD',16,9), +(2017,'Audi','S5','3.0T quattro Coupe AWD',16,5), +(2017,'Audi','S5','3.0T quattro Dynamic Edition Coupe AWD',16,9), +(2017,'Audi','S5','3.0T quattro Dynamic Edition Coupe AWD',16,5), +(2017,'Audi','S5','3.0T quattro Progressiv Cabriolet AWD',16,9), +(2017,'Audi','S5','3.0T quattro Progressiv Cabriolet AWD',16,5), +(2017,'Audi','S5','3.0T quattro Progressiv Coupe AWD',16,9), +(2017,'Audi','S5','3.0T quattro Progressiv Coupe AWD',16,5), +(2017,'Audi','S5','3.0T quattro Technik Cabriolet AWD',16,9), +(2017,'Audi','S5','3.0T quattro Technik Cabriolet AWD',16,5), +(2017,'Audi','S5','3.0T quattro Technik Coupe AWD',16,9), +(2017,'Audi','S5','3.0T quattro Technik Coupe AWD',16,5), +(2017,'Audi','S6','4.0T',1,1), +(2017,'Audi','S6','4.0T',1,2), +(2017,'Audi','S6','4.0T Premium Plus',1,1), +(2017,'Audi','S6','4.0T Premium Plus',1,2), +(2017,'Audi','S6','4.0T quattro Premium Plus Sedan AWD',18,5), +(2017,'Audi','S6','4.0T quattro Prestige Sedan AWD',18,5), +(2017,'Audi','S6','4.0T quattro Sedan AWD',18,5), +(2017,'Audi','S7','4.0T',1,1), +(2017,'Audi','S7','4.0T',1,2), +(2017,'Audi','S7','4.0T Premium Plus',1,1), +(2017,'Audi','S7','4.0T Premium Plus',1,2), +(2017,'Audi','S7','4.0T quattro AWD',18,5), +(2017,'Audi','S7','4.0T quattro Premium Plus AWD',18,5), +(2017,'Audi','S7','4.0T quattro Prestige AWD',18,5), +(2017,'Audi','S8','4.0T',1,1), +(2017,'Audi','S8','4.0T',1,2), +(2017,'Audi','S8','4.0T Plus',1,1), +(2017,'Audi','S8','4.0T Plus',1,2), +(2017,'Audi','S8','Plus 4.0T quattro AWD',26,10), +(2017,'Audi','SQ5','3.0T Premium Plus',1,1), +(2017,'Audi','SQ5','3.0T Premium Plus',1,2), +(2017,'Audi','SQ5','3.0T Prestige',1,1), +(2017,'Audi','SQ5','3.0T Prestige',1,2), +(2017,'Audi','SQ5','3.0T quattro Dynamic Edition AWD',28,10), +(2017,'Audi','SQ5','3.0T quattro Premium Plus AWD',28,10), +(2017,'Audi','SQ5','3.0T quattro Prestige AWD',28,10), +(2017,'Audi','SQ5','3.0T quattro Progressiv AWD',28,10), +(2017,'Audi','SQ5','3.0T quattro Technik AWD',28,10), +(2017,'Audi','TT','2.0T',1,1), +(2017,'Audi','TT','2.0T',1,2), +(2017,'Audi','TT','2.0T quattro Coupe AWD',10,8), +(2017,'Audi','TT','2.0T quattro Roadster AWD',10,8), +(2017,'Audi','TTS','2.0T',1,1), +(2017,'Audi','TTS','2.0T',1,2), +(2017,'Audi','TTS','2.0T quattro Coupe AWD',27,8), +(2017,'BMW','2 Series','230i Convertible RWD',29,10), +(2017,'BMW','2 Series','230i Coupe RWD',29,9), +(2017,'BMW','2 Series','230i Coupe RWD',29,10), +(2017,'BMW','2 Series','230i xDrive Convertible AWD',29,10), +(2017,'BMW','2 Series','230i xDrive Coupe AWD',29,10), +(2017,'BMW','2 Series','M240i Convertible RWD',30,9), +(2017,'BMW','2 Series','M240i Convertible RWD',30,10), +(2017,'BMW','2 Series','M240i Coupe RWD',30,9), +(2017,'BMW','2 Series','M240i Coupe RWD',30,10), +(2017,'BMW','2 Series','M240i xDrive Convertible AWD',30,10), +(2017,'BMW','2 Series','M240i xDrive Coupe AWD',30,10), +(2017,'BMW','230','i',1,1), +(2017,'BMW','230','i',1,2), +(2017,'BMW','230','i xDrive',1,1), +(2017,'BMW','230','i xDrive',1,2), +(2017,'BMW','3 Series','320i Sedan RWD',31,9), +(2017,'BMW','3 Series','320i Sedan RWD',31,10), +(2017,'BMW','3 Series','320i xDrive Sedan AWD',31,10), +(2017,'BMW','3 Series','328d Sedan RWD',32,10), +(2017,'BMW','3 Series','328d xDrive Sedan AWD',32,10), +(2017,'BMW','3 Series','328d xDrive Wagon AWD',32,10), +(2017,'BMW','3 Series','330e iPerformance Sedan RWD',29,10), +(2017,'BMW','3 Series','330i Sedan RWD',29,9), +(2017,'BMW','3 Series','330i Sedan RWD',29,10), +(2017,'BMW','3 Series','330i xDrive Sedan AWD',29,10), +(2017,'BMW','3 Series','330i xDrive Wagon AWD',29,10), +(2017,'BMW','3 Series','340i Sedan RWD',33,9), +(2017,'BMW','3 Series','340i Sedan RWD',33,10), +(2017,'BMW','3 Series','340i xDrive Sedan AWD',33,9), +(2017,'BMW','3 Series','340i xDrive Sedan AWD',33,10), +(2017,'BMW','3 Series Gran Turismo','330i xDrive AWD',29,10), +(2017,'BMW','3 Series Gran Turismo','340i xDrive AWD',33,10), +(2017,'BMW','320','i',1,1), +(2017,'BMW','320','i',1,2), +(2017,'BMW','320','i xDrive',1,1), +(2017,'BMW','320','i xDrive',1,2), +(2017,'BMW','328d','Base',1,1), +(2017,'BMW','328d','Base',1,2), +(2017,'BMW','328d','xDrive',1,1), +(2017,'BMW','328d','xDrive',1,2), +(2017,'BMW','330','i',1,1), +(2017,'BMW','330','i',1,2), +(2017,'BMW','330','i xDrive',1,1), +(2017,'BMW','330','i xDrive',1,2), +(2017,'BMW','330 Gran Turismo','i',1,1), +(2017,'BMW','330 Gran Turismo','i',1,2), +(2017,'BMW','330 Gran Turismo','i xDrive',1,1), +(2017,'BMW','330 Gran Turismo','i xDrive',1,2), +(2017,'BMW','330e','iPerformance',1,1), +(2017,'BMW','330e','iPerformance',1,2), +(2017,'BMW','340','i',1,1), +(2017,'BMW','340','i',1,2), +(2017,'BMW','340','i xDrive',1,1), +(2017,'BMW','340','i xDrive',1,2), +(2017,'BMW','340 Gran Turismo','i',1,1), +(2017,'BMW','340 Gran Turismo','i',1,2), +(2017,'BMW','340 Gran Turismo','i xDrive',1,1), +(2017,'BMW','340 Gran Turismo','i xDrive',1,2), +(2017,'BMW','4 Series','430i Convertible RWD',29,10), +(2017,'BMW','4 Series','430i Coupe RWD',29,9), +(2017,'BMW','4 Series','430i Coupe RWD',29,10), +(2017,'BMW','4 Series','430i Gran Coupe RWD',29,10), +(2017,'BMW','4 Series','430i xDrive Convertible AWD',29,10), +(2017,'BMW','4 Series','430i xDrive Coupe AWD',29,10), +(2017,'BMW','4 Series','430i xDrive Gran Coupe AWD',29,10), +(2017,'BMW','4 Series','440i Convertible RWD',33,10), +(2017,'BMW','4 Series','440i Coupe RWD',33,9), +(2017,'BMW','4 Series','440i Coupe RWD',33,10), +(2017,'BMW','4 Series','440i Gran Coupe RWD',33,10), +(2017,'BMW','4 Series','440i xDrive Convertible AWD',33,10), +(2017,'BMW','4 Series','440i xDrive Coupe AWD',33,9), +(2017,'BMW','4 Series','440i xDrive Coupe AWD',33,10), +(2017,'BMW','4 Series','440i xDrive Gran Coupe AWD',33,10), +(2017,'BMW','430','i',1,1), +(2017,'BMW','430','i',1,2), +(2017,'BMW','430','i xDrive',1,1), +(2017,'BMW','430','i xDrive',1,2), +(2017,'BMW','430 Gran Coupe','i',1,1), +(2017,'BMW','430 Gran Coupe','i',1,2), +(2017,'BMW','430 Gran Coupe','i xDrive',1,1), +(2017,'BMW','430 Gran Coupe','i xDrive',1,2), +(2017,'BMW','440','i',1,1), +(2017,'BMW','440','i',1,2), +(2017,'BMW','440','i xDrive',1,1), +(2017,'BMW','440','i xDrive',1,2), +(2017,'BMW','440 Gran Coupe','i',1,1), +(2017,'BMW','440 Gran Coupe','i',1,2), +(2017,'BMW','440 Gran Coupe','i xDrive',1,1), +(2017,'BMW','440 Gran Coupe','i xDrive',1,2), +(2017,'BMW','5 Series','530i Sedan RWD',29,10), +(2017,'BMW','5 Series','530i xDrive Sedan AWD',29,10), +(2017,'BMW','5 Series','540i Sedan RWD',30,10), +(2017,'BMW','5 Series','540i xDrive Sedan AWD',30,10), +(2017,'BMW','5 Series Gran Turismo','535i RWD',34,10), +(2017,'BMW','5 Series Gran Turismo','535i xDrive AWD',34,10), +(2017,'BMW','5 Series Gran Turismo','550i xDrive AWD',35,10), +(2017,'BMW','530','i',1,1), +(2017,'BMW','530','i',1,2), +(2017,'BMW','530','i xDrive',1,1), +(2017,'BMW','530','i xDrive',1,2), +(2017,'BMW','535 Gran Turismo','i',1,1), +(2017,'BMW','535 Gran Turismo','i',1,2), +(2017,'BMW','535 Gran Turismo','i xDrive',1,1), +(2017,'BMW','535 Gran Turismo','i xDrive',1,2), +(2017,'BMW','540','i',1,1), +(2017,'BMW','540','i',1,2), +(2017,'BMW','540','i xDrive',1,1), +(2017,'BMW','540','i xDrive',1,2), +(2017,'BMW','550 Gran Turismo','i xDrive',1,1), +(2017,'BMW','550 Gran Turismo','i xDrive',1,2), +(2017,'BMW','6 Series','640i Convertible RWD',36,10), +(2017,'BMW','6 Series','640i Coupe RWD',36,10), +(2017,'BMW','6 Series','640i Gran Coupe RWD',36,10), +(2017,'BMW','6 Series','640i xDrive Convertible AWD',36,10), +(2017,'BMW','6 Series','640i xDrive Coupe AWD',36,10), +(2017,'BMW','6 Series','640i xDrive Gran Coupe AWD',36,10), +(2017,'BMW','6 Series','650i Convertible RWD',35,10), +(2017,'BMW','6 Series','650i Coupe RWD',35,10), +(2017,'BMW','6 Series','650i Gran Coupe RWD',35,10), +(2017,'BMW','6 Series','650i xDrive Convertible AWD',35,10), +(2017,'BMW','6 Series','650i xDrive Coupe AWD',35,10), +(2017,'BMW','6 Series','650i xDrive Gran Coupe AWD',35,10), +(2017,'BMW','6 Series','Alpina B6 xDrive Gran Coupe AWD',37,10), +(2017,'BMW','640','i',1,1), +(2017,'BMW','640','i',1,2), +(2017,'BMW','640','i xDrive',1,1), +(2017,'BMW','640','i xDrive',1,2), +(2017,'BMW','640 Gran Coupe','i',1,1), +(2017,'BMW','640 Gran Coupe','i',1,2), +(2017,'BMW','640 Gran Coupe','i xDrive',1,1), +(2017,'BMW','640 Gran Coupe','i xDrive',1,2), +(2017,'BMW','650','i',1,1), +(2017,'BMW','650','i',1,2), +(2017,'BMW','650','i xDrive',1,1), +(2017,'BMW','650','i xDrive',1,2), +(2017,'BMW','650 Gran Coupe','i',1,1), +(2017,'BMW','650 Gran Coupe','i',1,2), +(2017,'BMW','650 Gran Coupe','i xDrive',1,1), +(2017,'BMW','650 Gran Coupe','i xDrive',1,2), +(2017,'BMW','7 Series','740e xDrive iPerformance AWD',38,10), +(2017,'BMW','7 Series','740i RWD',33,10), +(2017,'BMW','7 Series','740i xDrive AWD',33,10), +(2017,'BMW','7 Series','750Li xDrive AWD',39,10), +(2017,'BMW','7 Series','750i RWD',35,10), +(2017,'BMW','7 Series','750i xDrive AWD',35,10), +(2017,'BMW','7 Series','Alpina B7 xDrive AWD',37,10), +(2017,'BMW','7 Series','M760i xDrive AWD',39,10), +(2017,'BMW','740','i',1,1), +(2017,'BMW','740','i',1,2), +(2017,'BMW','740','i xDrive',1,1), +(2017,'BMW','740','i xDrive',1,2), +(2017,'BMW','740e','xDrive iPerformance',1,1), +(2017,'BMW','740e','xDrive iPerformance',1,2), +(2017,'BMW','750','i',1,1), +(2017,'BMW','750','i',1,2), +(2017,'BMW','750','i xDrive',1,1), +(2017,'BMW','750','i xDrive',1,2), +(2017,'BMW','ALPINA B6 Gran Coupe','Base',1,1), +(2017,'BMW','ALPINA B6 Gran Coupe','Base',1,2), +(2017,'BMW','ALPINA B7','xDrive',1,1), +(2017,'BMW','ALPINA B7','xDrive',1,2), +(2017,'BMW','M2','Base',1,1), +(2017,'BMW','M2','Base',1,2), +(2017,'BMW','M2','RWD',40,9), +(2017,'BMW','M2','RWD',40,5), +(2017,'BMW','M240','i',1,1), +(2017,'BMW','M240','i',1,2), +(2017,'BMW','M240','i xDrive',1,1), +(2017,'BMW','M240','i xDrive',1,2), +(2017,'BMW','M3','30 Jahre Edition Sedan RWD',41,9), +(2017,'BMW','M3','30 Jahre Edition Sedan RWD',41,5), +(2017,'BMW','M3','30 Jahre Edition Sedan RWD',1,1), +(2017,'BMW','M3','30 Jahre Edition Sedan RWD',1,2), +(2017,'BMW','M3','Base',1,1), +(2017,'BMW','M3','Base',1,2), +(2017,'BMW','M3','Sedan RWD',41,9), +(2017,'BMW','M3','Sedan RWD',41,5), +(2017,'BMW','M4','Base',1,1), +(2017,'BMW','M4','Base',1,2), +(2017,'BMW','M4','Convertible RWD',41,9), +(2017,'BMW','M4','Convertible RWD',41,5), +(2017,'BMW','M4','Coupe RWD',41,9), +(2017,'BMW','M4','Coupe RWD',41,5), +(2017,'BMW','M6','Base',1,1), +(2017,'BMW','M6','Base',1,2), +(2017,'BMW','M6','Convertible RWD',42,9), +(2017,'BMW','M6','Convertible RWD',42,5), +(2017,'BMW','M6','Coupe RWD',42,9), +(2017,'BMW','M6','Coupe RWD',42,5), +(2017,'BMW','M6','Gran Coupe RWD',42,9), +(2017,'BMW','M6','Gran Coupe RWD',42,5), +(2017,'BMW','M6 Gran Coupe','Base',1,1), +(2017,'BMW','M6 Gran Coupe','Base',1,2), +(2017,'BMW','M760','i xDrive',1,1), +(2017,'BMW','M760','i xDrive',1,2), +(2017,'BMW','X1','sDrive 28i',1,1), +(2017,'BMW','X1','sDrive 28i',1,2), +(2017,'BMW','X1','sDrive28i',1,1), +(2017,'BMW','X1','sDrive28i',1,2), +(2017,'BMW','X1','sDrive28i FWD',43,10), +(2017,'BMW','X1','xDrive 28i',1,1), +(2017,'BMW','X1','xDrive 28i',1,2), +(2017,'BMW','X1','xDrive28i AWD',43,10), +(2017,'BMW','X3','sDrive28i',1,1), +(2017,'BMW','X3','sDrive28i',1,2), +(2017,'BMW','X3','sDrive28i RWD',44,10), +(2017,'BMW','X3','xDrive28d',1,1), +(2017,'BMW','X3','xDrive28d',1,2), +(2017,'BMW','X3','xDrive28d AWD',32,10), +(2017,'BMW','X3','xDrive28d AWD',34,10), +(2017,'BMW','X3','xDrive28i',1,1), +(2017,'BMW','X3','xDrive28i',1,2), +(2017,'BMW','X3','xDrive28i AWD',44,10), +(2017,'BMW','X3','xDrive35i',1,1), +(2017,'BMW','X3','xDrive35i',1,2), +(2017,'BMW','X3','xDrive35i AWD',34,10), +(2017,'BMW','X4','M40i',1,1), +(2017,'BMW','X4','M40i',1,2), +(2017,'BMW','X4','M40i AWD',45,10), +(2017,'BMW','X4','xDrive 28i',1,1), +(2017,'BMW','X4','xDrive 28i',1,2), +(2017,'BMW','X4','xDrive28i AWD',44,10), +(2017,'BMW','X5','sDrive35i',1,1), +(2017,'BMW','X5','sDrive35i',1,2), +(2017,'BMW','X5','sDrive35i RWD',34,10), +(2017,'BMW','X5','xDrive35d',1,1), +(2017,'BMW','X5','xDrive35d',1,2), +(2017,'BMW','X5','xDrive35d AWD',46,10), +(2017,'BMW','X5','xDrive35i',1,1), +(2017,'BMW','X5','xDrive35i',1,2), +(2017,'BMW','X5','xDrive35i AWD',34,10), +(2017,'BMW','X5','xDrive40e iPerformance AWD',47,10), +(2017,'BMW','X5','xDrive50i',1,1), +(2017,'BMW','X5','xDrive50i',1,2), +(2017,'BMW','X5','xDrive50i AWD',35,10), +(2017,'BMW','X5 M','AWD',48,10), +(2017,'BMW','X5 M','Base',1,1), +(2017,'BMW','X5 M','Base',1,2), +(2017,'BMW','X5 eDrive','xDrive40e',1,1), +(2017,'BMW','X5 eDrive','xDrive40e',1,2), +(2017,'BMW','X6','sDrive35i',1,1), +(2017,'BMW','X6','sDrive35i',1,2), +(2017,'BMW','X6','sDrive35i RWD',34,10), +(2017,'BMW','X6','xDrive35i',1,1), +(2017,'BMW','X6','xDrive35i',1,2), +(2017,'BMW','X6','xDrive35i AWD',34,10), +(2017,'BMW','X6','xDrive50i',1,1), +(2017,'BMW','X6','xDrive50i',1,2), +(2017,'BMW','X6','xDrive50i AWD',35,10), +(2017,'BMW','X6 M','AWD',48,10), +(2017,'BMW','X6 M','Base',1,1), +(2017,'BMW','X6 M','Base',1,2), +(2017,'BMW','i3','60 Ah',1,1), +(2017,'BMW','i3','60 Ah',1,2), +(2017,'BMW','i3','60 Ah RWD',49,11), +(2017,'BMW','i3','60 Ah RWD',50,11), +(2017,'BMW','i3','94 Ah',1,1), +(2017,'BMW','i3','94 Ah',1,2), +(2017,'BMW','i3','94 Ah RWD',51,11), +(2017,'BMW','i3','94 Ah RWD',50,11), +(2017,'BMW','i3','94 Ah RWD with Range Extender',49,11), +(2017,'BMW','i3','94 Ah RWD with Range Extender',52,11), +(2017,'BMW','i3','94 Ah w/Range Extender',1,1), +(2017,'BMW','i3','94 Ah w/Range Extender',1,2), +(2017,'BMW','i8','Base',1,1), +(2017,'BMW','i8','Base',1,2), +(2017,'BMW','i8','Coupe AWD',53,7), +(2017,'Bentley','Bentayga','W12',1,1), +(2017,'Bentley','Bentayga','W12',1,2), +(2017,'Bentley','Bentayga','W12 AWD',54,10), +(2017,'Bentley','Bentayga','W12 First Edition',1,1), +(2017,'Bentley','Bentayga','W12 First Edition',1,2), +(2017,'Bentley','Bentayga','W12 First Edition AWD',54,10), +(2017,'Bentley','Continental GT','Speed',1,1), +(2017,'Bentley','Continental GT','Speed',1,2), +(2017,'Bentley','Continental GT','Speed AWD',55,10), +(2017,'Bentley','Continental GT','Supersport',1,1), +(2017,'Bentley','Continental GT','Supersport',1,2), +(2017,'Bentley','Continental GT','V8',1,1), +(2017,'Bentley','Continental GT','V8',1,2), +(2017,'Bentley','Continental GT','V8 AWD',56,10), +(2017,'Bentley','Continental GT','V8 S',1,1), +(2017,'Bentley','Continental GT','V8 S',1,2), +(2017,'Bentley','Continental GT','V8 S AWD',57,10), +(2017,'Bentley','Continental GT','W12',1,1), +(2017,'Bentley','Continental GT','W12',1,2), +(2017,'Bentley','Continental GT','W12 AWD',58,10), +(2017,'Bentley','Continental GTC','Speed AWD',55,10), +(2017,'Bentley','Continental GTC','V8 AWD',56,10), +(2017,'Bentley','Continental GTC','V8 S AWD',57,10), +(2017,'Bentley','Continental GTC','W12 AWD',58,10), +(2017,'Bentley','Continental Supersports','Convertible AWD',59,10), +(2017,'Bentley','Continental Supersports','Coupe AWD',59,10), +(2017,'Bentley','Flying Spur','V8',1,1), +(2017,'Bentley','Flying Spur','V8',1,2), +(2017,'Bentley','Flying Spur','V8 AWD',56,10), +(2017,'Bentley','Flying Spur','V8 S',1,1), +(2017,'Bentley','Flying Spur','V8 S',1,2), +(2017,'Bentley','Flying Spur','V8 S AWD',57,10), +(2017,'Bentley','Flying Spur','W12',1,1), +(2017,'Bentley','Flying Spur','W12',1,2), +(2017,'Bentley','Flying Spur','W12 AWD',60,10), +(2017,'Bentley','Flying Spur','W12 S',1,1), +(2017,'Bentley','Flying Spur','W12 S',1,2), +(2017,'Bentley','Flying Spur','W12 S AWD',60,10), +(2017,'Bentley','Flying Spur','W12 S AWD',61,10), +(2017,'Bentley','Mulsanne','Base',1,1), +(2017,'Bentley','Mulsanne','Base',1,2), +(2017,'Bentley','Mulsanne','Extended Wheelbase RWD',62,10), +(2017,'Bentley','Mulsanne','RWD',62,10), +(2017,'Bentley','Mulsanne','Speed',1,1), +(2017,'Bentley','Mulsanne','Speed',1,2), +(2017,'Bentley','Mulsanne','Speed RWD',62,10), +(2017,'Bentley','Mulsanne','Speed RWD',63,10), +(2017,'Buick','Cascada','Base',1,1), +(2017,'Buick','Cascada','Base',1,2), +(2017,'Buick','Cascada','FWD',64,12), +(2017,'Buick','Cascada','Premium',1,1), +(2017,'Buick','Cascada','Premium',1,2), +(2017,'Buick','Cascada','Premium FWD',64,12), +(2017,'Buick','Cascada','Sport Touring',1,1), +(2017,'Buick','Cascada','Sport Touring',1,2), +(2017,'Buick','Cascada','Sport Touring FWD',64,12), +(2017,'Buick','Enclave','Convenience',1,1), +(2017,'Buick','Enclave','Convenience',1,2), +(2017,'Buick','Enclave','Convenience FWD',65,7), +(2017,'Buick','Enclave','Leather',1,1), +(2017,'Buick','Enclave','Leather',1,2), +(2017,'Buick','Enclave','Leather AWD',65,7), +(2017,'Buick','Enclave','Leather FWD',65,7), +(2017,'Buick','Enclave','Premium',1,1), +(2017,'Buick','Enclave','Premium',1,2), +(2017,'Buick','Enclave','Premium AWD',65,7), +(2017,'Buick','Enclave','Premium FWD',65,7), +(2017,'Buick','Encore','Base',1,1), +(2017,'Buick','Encore','Base',1,2), +(2017,'Buick','Encore','Essence',1,1), +(2017,'Buick','Encore','Essence',1,2), +(2017,'Buick','Encore','Essence AWD',66,7), +(2017,'Buick','Encore','Essence AWD',67,7), +(2017,'Buick','Encore','Essence FWD',66,7), +(2017,'Buick','Encore','Essence FWD',67,7), +(2017,'Buick','Encore','FWD',66,7), +(2017,'Buick','Encore','Preferred',1,1), +(2017,'Buick','Encore','Preferred',1,2), +(2017,'Buick','Encore','Preferred AWD',66,7), +(2017,'Buick','Encore','Preferred FWD',66,7), +(2017,'Buick','Encore','Preferred II',1,1), +(2017,'Buick','Encore','Preferred II',1,2), +(2017,'Buick','Encore','Preferred II AWD',66,7), +(2017,'Buick','Encore','Preferred II AWD',67,7), +(2017,'Buick','Encore','Preferred II FWD',66,7), +(2017,'Buick','Encore','Preferred II FWD',67,7), +(2017,'Buick','Encore','Premium',1,1), +(2017,'Buick','Encore','Premium',1,2), +(2017,'Buick','Encore','Premium AWD',66,7), +(2017,'Buick','Encore','Premium AWD',67,7), +(2017,'Buick','Encore','Premium FWD',66,7), +(2017,'Buick','Encore','Premium FWD',67,7), +(2017,'Buick','Encore','Sport Touring',1,1), +(2017,'Buick','Encore','Sport Touring',1,2), +(2017,'Buick','Encore','Sport Touring AWD',66,7), +(2017,'Buick','Encore','Sport Touring AWD',67,7), +(2017,'Buick','Encore','Sport Touring FWD',66,7), +(2017,'Buick','Encore','Sport Touring FWD',67,7), +(2017,'Buick','Envision','Base',1,1), +(2017,'Buick','Envision','Base',1,2), +(2017,'Buick','Envision','Convenience',1,1), +(2017,'Buick','Envision','Convenience',1,2), +(2017,'Buick','Envision','Essence',1,1), +(2017,'Buick','Envision','Essence',1,2), +(2017,'Buick','Envision','Essence AWD',68,7), +(2017,'Buick','Envision','Essence FWD',68,7), +(2017,'Buick','Envision','FWD',14,7), +(2017,'Buick','Envision','FWD',68,7), +(2017,'Buick','Envision','Leather',1,1), +(2017,'Buick','Envision','Leather',1,2), +(2017,'Buick','Envision','Preferred',1,1), +(2017,'Buick','Envision','Preferred',1,2), +(2017,'Buick','Envision','Preferred AWD',68,7), +(2017,'Buick','Envision','Preferred FWD',68,7), +(2017,'Buick','Envision','Premium I',1,1), +(2017,'Buick','Envision','Premium I',1,2), +(2017,'Buick','Envision','Premium I AWD',14,7), +(2017,'Buick','Envision','Premium II',1,1), +(2017,'Buick','Envision','Premium II',1,2), +(2017,'Buick','Envision','Premium II AWD',14,7), +(2017,'Buick','LaCrosse','Base',1,1), +(2017,'Buick','LaCrosse','Base',1,2), +(2017,'Buick','LaCrosse','Essence',1,1), +(2017,'Buick','LaCrosse','Essence',1,2), +(2017,'Buick','LaCrosse','Essence FWD',69,10), +(2017,'Buick','LaCrosse','FWD',69,10), +(2017,'Buick','LaCrosse','Preferred',1,1), +(2017,'Buick','LaCrosse','Preferred',1,2), +(2017,'Buick','LaCrosse','Preferred FWD',69,10), +(2017,'Buick','LaCrosse','Premium',1,1), +(2017,'Buick','LaCrosse','Premium',1,2), +(2017,'Buick','LaCrosse','Premium AWD',69,10), +(2017,'Buick','LaCrosse','Premium FWD',69,10), +(2017,'Buick','Regal','1SV',1,1), +(2017,'Buick','Regal','1SV',1,2), +(2017,'Buick','Regal','1SV Sedan FWD',70,7), +(2017,'Buick','Regal','1SV Sedan FWD',71,7), +(2017,'Buick','Regal','GS Sedan AWD',72,7), +(2017,'Buick','Regal','GS Sedan FWD',72,7), +(2017,'Buick','Regal','Premium I Sedan AWD',72,7), +(2017,'Buick','Regal','Premium I Sedan FWD',72,7), +(2017,'Buick','Regal','Premium II Sedan AWD',72,7), +(2017,'Buick','Regal','Premium II Sedan FWD',72,7), +(2017,'Buick','Regal','Sedan AWD',72,7), +(2017,'Buick','Regal','Sport Touring Sedan FWD',72,7), +(2017,'Buick','Regal','Turbo',1,1), +(2017,'Buick','Regal','Turbo',1,2), +(2017,'Buick','Regal','Turbo GS',1,1), +(2017,'Buick','Regal','Turbo GS',1,2), +(2017,'Buick','Regal','Turbo Premium II',1,1), +(2017,'Buick','Regal','Turbo Premium II',1,2), +(2017,'Buick','Regal','Turbo Sport Touring',1,1), +(2017,'Buick','Regal','Turbo Sport Touring',1,2), +(2017,'Buick','Verano','Base',1,1), +(2017,'Buick','Verano','Base',1,2), +(2017,'Buick','Verano','FWD',73,12), +(2017,'Buick','Verano','FWD',74,12), +(2017,'Buick','Verano','Leather FWD',73,12), +(2017,'Buick','Verano','Leather FWD',74,12), +(2017,'Buick','Verano','Leather Group',1,1), +(2017,'Buick','Verano','Leather Group',1,2), +(2017,'Buick','Verano','Sport Touring',1,1), +(2017,'Buick','Verano','Sport Touring',1,2), +(2017,'Buick','Verano','Sport Touring FWD',73,12), +(2017,'Buick','Verano','Sport Touring FWD',74,12), +(2017,'Cadillac','ATS','2.0L Turbo',1,1), +(2017,'Cadillac','ATS','2.0L Turbo',1,2), +(2017,'Cadillac','ATS','2.0L Turbo Luxury',1,1), +(2017,'Cadillac','ATS','2.0L Turbo Luxury',1,2), +(2017,'Cadillac','ATS','2.0L Turbo Premium Luxury',1,1), +(2017,'Cadillac','ATS','2.0L Turbo Premium Luxury',1,2), +(2017,'Cadillac','ATS','2.0L Turbo Premium Performance',1,1), +(2017,'Cadillac','ATS','2.0L Turbo Premium Performance',1,2), +(2017,'Cadillac','ATS','2.0T AWD',75,10), +(2017,'Cadillac','ATS','2.0T Luxury AWD',75,10), +(2017,'Cadillac','ATS','2.0T Luxury RWD',75,9), +(2017,'Cadillac','ATS','2.0T Luxury RWD',75,10), +(2017,'Cadillac','ATS','2.0T Premium Luxury AWD',75,10), +(2017,'Cadillac','ATS','2.0T Premium Luxury AWD',76,10), +(2017,'Cadillac','ATS','2.0T Premium Luxury RWD',75,9), +(2017,'Cadillac','ATS','2.0T Premium Luxury RWD',75,10), +(2017,'Cadillac','ATS','2.0T Premium Luxury RWD',76,10), +(2017,'Cadillac','ATS','2.0T Premium Performance RWD',75,9), +(2017,'Cadillac','ATS','2.0T Premium Performance RWD',75,10), +(2017,'Cadillac','ATS','2.0T Premium Performance RWD',76,10), +(2017,'Cadillac','ATS','2.0T RWD',75,9), +(2017,'Cadillac','ATS','2.0T RWD',75,10), +(2017,'Cadillac','ATS','2.5L Luxury',1,1), +(2017,'Cadillac','ATS','2.5L Luxury',1,2), +(2017,'Cadillac','ATS','2.5L Luxury RWD',77,10), +(2017,'Cadillac','ATS','2.5L Luxury RWD',76,10), +(2017,'Cadillac','ATS','2.5L RWD',77,10), +(2017,'Cadillac','ATS','2.5L RWD',76,10), +(2017,'Cadillac','ATS','3.6L Luxury',1,1), +(2017,'Cadillac','ATS','3.6L Luxury',1,2), +(2017,'Cadillac','ATS','3.6L Luxury AWD',76,10), +(2017,'Cadillac','ATS','3.6L Luxury RWD',76,10), +(2017,'Cadillac','ATS','3.6L Premium Luxury',1,1), +(2017,'Cadillac','ATS','3.6L Premium Luxury',1,2), +(2017,'Cadillac','ATS','3.6L Premium Luxury AWD',76,10), +(2017,'Cadillac','ATS','3.6L Premium Luxury RWD',76,10), +(2017,'Cadillac','ATS','3.6L Premium Performance',1,1), +(2017,'Cadillac','ATS','3.6L Premium Performance',1,2), +(2017,'Cadillac','ATS','3.6L Premium Performance RWD',76,10), +(2017,'Cadillac','ATS','Base',1,1), +(2017,'Cadillac','ATS','Base',1,2), +(2017,'Cadillac','ATS Coupe','2.0T AWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Luxury AWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Luxury RWD',75,9), +(2017,'Cadillac','ATS Coupe','2.0T Luxury RWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Luxury AWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Luxury AWD',76,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Luxury RWD',75,9), +(2017,'Cadillac','ATS Coupe','2.0T Premium Luxury RWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Luxury RWD',76,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Performance RWD',75,9), +(2017,'Cadillac','ATS Coupe','2.0T Premium Performance RWD',75,10), +(2017,'Cadillac','ATS Coupe','2.0T Premium Performance RWD',76,10), +(2017,'Cadillac','ATS Coupe','2.0T RWD',75,9), +(2017,'Cadillac','ATS Coupe','2.0T RWD',75,10), +(2017,'Cadillac','ATS Coupe','3.6L Luxury AWD',76,10), +(2017,'Cadillac','ATS Coupe','3.6L Luxury RWD',76,10), +(2017,'Cadillac','ATS Coupe','3.6L Premium Luxury AWD',76,10), +(2017,'Cadillac','ATS Coupe','3.6L Premium Luxury RWD',76,10), +(2017,'Cadillac','ATS Coupe','3.6L Premium Performance RWD',76,10), +(2017,'Cadillac','ATS-V','Base',1,1), +(2017,'Cadillac','ATS-V','Base',1,2), +(2017,'Cadillac','ATS-V','RWD',78,9), +(2017,'Cadillac','ATS-V','RWD',78,10), +(2017,'Cadillac','ATS-V Coupe','RWD',78,9), +(2017,'Cadillac','ATS-V Coupe','RWD',78,10), +(2017,'Cadillac','CT6','2.0L Turbo Base',1,1), +(2017,'Cadillac','CT6','2.0L Turbo Base',1,2), +(2017,'Cadillac','CT6','2.0L Turbo Luxury',1,1), +(2017,'Cadillac','CT6','2.0L Turbo Luxury',1,2), +(2017,'Cadillac','CT6','2.0T Luxury RWD',79,10), +(2017,'Cadillac','CT6','2.0T RWD',79,10), +(2017,'Cadillac','CT6','3.0L Twin Turbo Luxury',1,1), +(2017,'Cadillac','CT6','3.0L Twin Turbo Luxury',1,2), +(2017,'Cadillac','CT6','3.0L Twin Turbo Platinum',1,1), +(2017,'Cadillac','CT6','3.0L Twin Turbo Platinum',1,2), +(2017,'Cadillac','CT6','3.0L Twin Turbo Premium Luxury',1,1), +(2017,'Cadillac','CT6','3.0L Twin Turbo Premium Luxury',1,2), +(2017,'Cadillac','CT6','3.0TT Luxury AWD',80,10), +(2017,'Cadillac','CT6','3.0TT Platinum AWD',80,10), +(2017,'Cadillac','CT6','3.0TT Premium Luxury AWD',80,10), +(2017,'Cadillac','CT6','3.6L AWD',76,10), +(2017,'Cadillac','CT6','3.6L Base',1,1), +(2017,'Cadillac','CT6','3.6L Base',1,2), +(2017,'Cadillac','CT6','3.6L Luxury',1,1), +(2017,'Cadillac','CT6','3.6L Luxury',1,2), +(2017,'Cadillac','CT6','3.6L Luxury AWD',76,10), +(2017,'Cadillac','CT6','3.6L Platinum',1,1), +(2017,'Cadillac','CT6','3.6L Platinum',1,2), +(2017,'Cadillac','CT6','3.6L Platinum AWD',76,10), +(2017,'Cadillac','CT6','3.6L Premium Luxury',1,1), +(2017,'Cadillac','CT6','3.6L Premium Luxury',1,2), +(2017,'Cadillac','CT6','3.6L Premium Luxury AWD',76,10), +(2017,'Cadillac','CT6 Hybrid Plug-In','RWD',81,13), +(2017,'Cadillac','CT6 PLUG-IN','Base',1,1), +(2017,'Cadillac','CT6 PLUG-IN','Base',1,2), +(2017,'Cadillac','CTS','2.0L Turbo',1,1), +(2017,'Cadillac','CTS','2.0L Turbo',1,2), +(2017,'Cadillac','CTS','2.0L Turbo Luxury',1,1), +(2017,'Cadillac','CTS','2.0L Turbo Luxury',1,2), +(2017,'Cadillac','CTS','2.0T AWD',82,10), +(2017,'Cadillac','CTS','2.0T Luxury AWD',82,10), +(2017,'Cadillac','CTS','2.0T Luxury RWD',82,10), +(2017,'Cadillac','CTS','2.0T RWD',82,10), +(2017,'Cadillac','CTS','3.6L Luxury',1,1), +(2017,'Cadillac','CTS','3.6L Luxury',1,2), +(2017,'Cadillac','CTS','3.6L Luxury AWD',76,10), +(2017,'Cadillac','CTS','3.6L Luxury RWD',76,10), +(2017,'Cadillac','CTS','3.6L Premium Luxury',1,1), +(2017,'Cadillac','CTS','3.6L Premium Luxury',1,2), +(2017,'Cadillac','CTS','3.6L Premium Luxury AWD',76,10), +(2017,'Cadillac','CTS','3.6L Premium Luxury RWD',76,10), +(2017,'Cadillac','CTS','3.6L Twin Turbo V-Sport Premium Luxury',1,1), +(2017,'Cadillac','CTS','3.6L Twin Turbo V-Sport Premium Luxury',1,2), +(2017,'Cadillac','CTS','3.6L Twin Turbo Vsport',1,1), +(2017,'Cadillac','CTS','3.6L Twin Turbo Vsport',1,2), +(2017,'Cadillac','CTS','3.6TT V-Sport Premium Luxury RWD',83,10), +(2017,'Cadillac','CTS','3.6TT V-Sport RWD',83,10), +(2017,'Cadillac','CTS-V','Base',1,1), +(2017,'Cadillac','CTS-V','Base',1,2), +(2017,'Cadillac','CTS-V','RWD',84,10), +(2017,'Cadillac','Escalade','4WD',85,10), +(2017,'Cadillac','Escalade','Base',1,1), +(2017,'Cadillac','Escalade','Base',1,2), +(2017,'Cadillac','Escalade','Luxury',1,1), +(2017,'Cadillac','Escalade','Luxury',1,2), +(2017,'Cadillac','Escalade','Luxury 4WD',85,10), +(2017,'Cadillac','Escalade','Luxury RWD',85,10), +(2017,'Cadillac','Escalade','Platinum',1,1), +(2017,'Cadillac','Escalade','Platinum',1,2), +(2017,'Cadillac','Escalade','Platinum 4WD',85,10), +(2017,'Cadillac','Escalade','Platinum RWD',85,10), +(2017,'Cadillac','Escalade','Premium Luxury',1,1), +(2017,'Cadillac','Escalade','Premium Luxury',1,2), +(2017,'Cadillac','Escalade','Premium Luxury 4WD',85,10), +(2017,'Cadillac','Escalade','Premium Luxury RWD',85,10), +(2017,'Cadillac','Escalade','RWD',85,10), +(2017,'Cadillac','Escalade ESV','4WD',85,10), +(2017,'Cadillac','Escalade ESV','Base',1,1), +(2017,'Cadillac','Escalade ESV','Base',1,2), +(2017,'Cadillac','Escalade ESV','Luxury',1,1), +(2017,'Cadillac','Escalade ESV','Luxury',1,2), +(2017,'Cadillac','Escalade ESV','Luxury 4WD',85,10), +(2017,'Cadillac','Escalade ESV','Luxury RWD',85,10), +(2017,'Cadillac','Escalade ESV','Platinum',1,1), +(2017,'Cadillac','Escalade ESV','Platinum',1,2), +(2017,'Cadillac','Escalade ESV','Platinum 4WD',85,10), +(2017,'Cadillac','Escalade ESV','Platinum RWD',85,10), +(2017,'Cadillac','Escalade ESV','Premium Collection',1,1), +(2017,'Cadillac','Escalade ESV','Premium Collection',1,2), +(2017,'Cadillac','Escalade ESV','Premium Luxury',1,1), +(2017,'Cadillac','Escalade ESV','Premium Luxury',1,2), +(2017,'Cadillac','Escalade ESV','Premium Luxury 4WD',85,10), +(2017,'Cadillac','Escalade ESV','Premium Luxury RWD',85,10), +(2017,'Cadillac','Escalade ESV','RWD',85,10), +(2017,'Cadillac','XT5','Base',1,1), +(2017,'Cadillac','XT5','Base',1,2), +(2017,'Cadillac','XT5','FWD',69,10), +(2017,'Cadillac','XT5','Luxury',1,1), +(2017,'Cadillac','XT5','Luxury',1,2), +(2017,'Cadillac','XT5','Luxury AWD',69,10), +(2017,'Cadillac','XT5','Luxury FWD',69,10), +(2017,'Cadillac','XT5','Platinum',1,1), +(2017,'Cadillac','XT5','Platinum',1,2), +(2017,'Cadillac','XT5','Platinum AWD',69,10), +(2017,'Cadillac','XT5','Premium Luxury',1,1), +(2017,'Cadillac','XT5','Premium Luxury',1,2), +(2017,'Cadillac','XT5','Premium Luxury AWD',69,10), +(2017,'Cadillac','XT5','Premium Luxury FWD',69,10), +(2017,'Cadillac','XTS','Base',1,1), +(2017,'Cadillac','XTS','Base',1,2), +(2017,'Cadillac','XTS','FWD',86,7), +(2017,'Cadillac','XTS','Luxury',1,1), +(2017,'Cadillac','XTS','Luxury',1,2), +(2017,'Cadillac','XTS','Luxury AWD',86,7), +(2017,'Cadillac','XTS','Luxury FWD',86,7), +(2017,'Cadillac','XTS','Platinum',1,1), +(2017,'Cadillac','XTS','Platinum',1,2); + +INSERT INTO vehicle_options (year, make, model, trim, engine_id, transmission_id) VALUES +(2017,'Cadillac','XTS','Platinum AWD',86,7), +(2017,'Cadillac','XTS','Platinum FWD',86,7), +(2017,'Cadillac','XTS','Platinum V-Sport AWD',87,7), +(2017,'Cadillac','XTS','Premium Luxury',1,1), +(2017,'Cadillac','XTS','Premium Luxury',1,2), +(2017,'Cadillac','XTS','Premium Luxury AWD',86,7), +(2017,'Cadillac','XTS','Premium Luxury FWD',86,7), +(2017,'Cadillac','XTS','Premium Luxury V-Sport AWD',86,7), +(2017,'Cadillac','XTS','Premium Luxury V-Sport AWD',87,7), +(2017,'Cadillac','XTS','Pro Armored FWD',86,7), +(2017,'Cadillac','XTS','Pro Coachbuilder Funeral FWD',86,7), +(2017,'Cadillac','XTS','Pro Coachbuilder Limousine FWD',86,7), +(2017,'Cadillac','XTS','Pro Coachbuilder Stretch Livery FWD',86,7), +(2017,'Cadillac','XTS','Pro Livery FWD',86,7), +(2017,'Cadillac','XTS','V-Sport Platinum Twin Turbo',1,1), +(2017,'Cadillac','XTS','V-Sport Platinum Twin Turbo',1,2), +(2017,'Cadillac','XTS','V-Sport Premium Twin Turbo',1,1), +(2017,'Cadillac','XTS','V-Sport Premium Twin Turbo',1,2), +(2017,'Chevrolet','Bolt EV','LT FWD',88,11), +(2017,'Chevrolet','Bolt EV','Premier FWD',88,11), +(2017,'Chevrolet','Camaro','1LS',1,1), +(2017,'Chevrolet','Camaro','1LS',1,2), +(2017,'Chevrolet','Camaro','1LT',1,1), +(2017,'Chevrolet','Camaro','1LT',1,2), +(2017,'Chevrolet','Camaro','1LT Convertible RWD',89,9), +(2017,'Chevrolet','Camaro','1LT Convertible RWD',89,10), +(2017,'Chevrolet','Camaro','1LT Convertible RWD',76,9), +(2017,'Chevrolet','Camaro','1LT Convertible RWD',76,10), +(2017,'Chevrolet','Camaro','1LT Coupe RWD',89,9), +(2017,'Chevrolet','Camaro','1LT Coupe RWD',89,10), +(2017,'Chevrolet','Camaro','1LT Coupe RWD',76,9), +(2017,'Chevrolet','Camaro','1LT Coupe RWD',76,10), +(2017,'Chevrolet','Camaro','1SS',1,1), +(2017,'Chevrolet','Camaro','1SS',1,2), +(2017,'Chevrolet','Camaro','1SS Convertible RWD',90,9), +(2017,'Chevrolet','Camaro','1SS Convertible RWD',90,10), +(2017,'Chevrolet','Camaro','1SS Coupe RWD',90,9), +(2017,'Chevrolet','Camaro','1SS Coupe RWD',90,10), +(2017,'Chevrolet','Camaro','2LT',1,1), +(2017,'Chevrolet','Camaro','2LT',1,2), +(2017,'Chevrolet','Camaro','2LT Convertible RWD',89,9), +(2017,'Chevrolet','Camaro','2LT Convertible RWD',89,10), +(2017,'Chevrolet','Camaro','2LT Convertible RWD',76,9), +(2017,'Chevrolet','Camaro','2LT Convertible RWD',76,10), +(2017,'Chevrolet','Camaro','2LT Coupe RWD',89,9), +(2017,'Chevrolet','Camaro','2LT Coupe RWD',89,10), +(2017,'Chevrolet','Camaro','2LT Coupe RWD',76,9), +(2017,'Chevrolet','Camaro','2LT Coupe RWD',76,10), +(2017,'Chevrolet','Camaro','2SS',1,1), +(2017,'Chevrolet','Camaro','2SS',1,2), +(2017,'Chevrolet','Camaro','2SS Convertible RWD',90,9), +(2017,'Chevrolet','Camaro','2SS Convertible RWD',90,10), +(2017,'Chevrolet','Camaro','2SS Coupe RWD',90,9), +(2017,'Chevrolet','Camaro','2SS Coupe RWD',90,10), +(2017,'Chevrolet','Camaro','LS Convertible RWD',89,9), +(2017,'Chevrolet','Camaro','LS Convertible RWD',76,9), +(2017,'Chevrolet','Camaro','LS Convertible RWD',91,14), +(2017,'Chevrolet','Camaro','LS Convertible RWD',91,9), +(2017,'Chevrolet','Camaro','LS Coupe RWD',89,9), +(2017,'Chevrolet','Camaro','LS Coupe RWD',76,9), +(2017,'Chevrolet','Camaro','ZL1',1,1), +(2017,'Chevrolet','Camaro','ZL1',1,2), +(2017,'Chevrolet','Camaro','ZL1 Convertible RWD',91,14), +(2017,'Chevrolet','Camaro','ZL1 Convertible RWD',91,9), +(2017,'Chevrolet','Camaro','ZL1 Coupe RWD',91,14), +(2017,'Chevrolet','Camaro','ZL1 Coupe RWD',91,9), +(2017,'Chevrolet','Caprice','Police Sedan RWD',92,7), +(2017,'Chevrolet','Caprice','Police Sedan RWD',93,7), +(2017,'Chevrolet','City Express','1LS',1,1), +(2017,'Chevrolet','City Express','1LS',1,2), +(2017,'Chevrolet','City Express','1LT',1,1), +(2017,'Chevrolet','City Express','1LT',1,2), +(2017,'Chevrolet','City Express','LS FWD',94,15), +(2017,'Chevrolet','City Express','LT FWD',94,15), +(2017,'Chevrolet','Colorado','Base',1,1), +(2017,'Chevrolet','Colorado','Base',1,2), +(2017,'Chevrolet','Colorado','Base Extended Cab LB RWD',95,9), +(2017,'Chevrolet','Colorado','LT',1,1), +(2017,'Chevrolet','Colorado','LT',1,2), +(2017,'Chevrolet','Colorado','LT Crew Cab 4WD',96,7), +(2017,'Chevrolet','Colorado','LT Crew Cab 4WD',96,10), +(2017,'Chevrolet','Colorado','LT Crew Cab 4WD',97,7), +(2017,'Chevrolet','Colorado','LT Crew Cab 4WD',97,10), +(2017,'Chevrolet','Colorado','LT Crew Cab LB 4WD',96,7), +(2017,'Chevrolet','Colorado','LT Crew Cab LB 4WD',96,10), +(2017,'Chevrolet','Colorado','LT Crew Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','LT Crew Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','LT Crew Cab LB RWD',96,7), +(2017,'Chevrolet','Colorado','LT Crew Cab LB RWD',96,10), +(2017,'Chevrolet','Colorado','LT Crew Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','LT Crew Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',95,7), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',95,10), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',96,7), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',96,10), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',97,7), +(2017,'Chevrolet','Colorado','LT Crew Cab RWD',97,10), +(2017,'Chevrolet','Colorado','LT Extended Cab LB 4WD',95,7), +(2017,'Chevrolet','Colorado','LT Extended Cab LB 4WD',95,10), +(2017,'Chevrolet','Colorado','LT Extended Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','LT Extended Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','LT Extended Cab LB RWD',95,7), +(2017,'Chevrolet','Colorado','LT Extended Cab LB RWD',95,10), +(2017,'Chevrolet','Colorado','LT Extended Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','LT Extended Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','WT',1,1), +(2017,'Chevrolet','Colorado','WT',1,2), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab 4WD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab 4WD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab 4WD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab 4WD',97,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB 4WD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB 4WD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB RWD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB RWD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',95,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',95,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Crew Cab RWD',97,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',95,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',95,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',95,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',95,9), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',95,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',96,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',96,9), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',96,10), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',97,9), +(2017,'Chevrolet','Colorado','Work Truck Extended Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','Z71',1,1), +(2017,'Chevrolet','Colorado','Z71',1,2), +(2017,'Chevrolet','Colorado','Z71 Crew Cab 4WD',96,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab 4WD',96,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab 4WD',97,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab 4WD',97,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB 4WD',96,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB 4WD',96,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB RWD',96,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB RWD',96,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',95,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',95,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',96,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',96,10), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',97,7), +(2017,'Chevrolet','Colorado','Z71 Crew Cab RWD',97,10), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB 4WD',95,7), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB 4WD',95,10), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB 4WD',97,10), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB RWD',95,7), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB RWD',95,10), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB RWD',97,7), +(2017,'Chevrolet','Colorado','Z71 Extended Cab LB RWD',97,10), +(2017,'Chevrolet','Colorado','ZR2',1,1), +(2017,'Chevrolet','Colorado','ZR2',1,2), +(2017,'Chevrolet','Colorado','ZR2 Crew Cab 4WD',96,7), +(2017,'Chevrolet','Colorado','ZR2 Crew Cab 4WD',96,10), +(2017,'Chevrolet','Colorado','ZR2 Crew Cab 4WD',97,7), +(2017,'Chevrolet','Colorado','ZR2 Crew Cab 4WD',97,10), +(2017,'Chevrolet','Colorado','ZR2 Extended Cab LB 4WD',96,7), +(2017,'Chevrolet','Colorado','ZR2 Extended Cab LB 4WD',96,10), +(2017,'Chevrolet','Colorado','ZR2 Extended Cab LB 4WD',97,7), +(2017,'Chevrolet','Colorado','ZR2 Extended Cab LB 4WD',97,10), +(2017,'Chevrolet','Corvette','Grand Sport',1,1), +(2017,'Chevrolet','Corvette','Grand Sport',1,2), +(2017,'Chevrolet','Corvette','Grand Sport 1LT Convertible RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 1LT Convertible RWD',98,10), +(2017,'Chevrolet','Corvette','Grand Sport 1LT Coupe RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 1LT Coupe RWD',98,10), +(2017,'Chevrolet','Corvette','Grand Sport 2LT Convertible RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 2LT Convertible RWD',98,10), +(2017,'Chevrolet','Corvette','Grand Sport 2LT Coupe RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 2LT Coupe RWD',98,10), +(2017,'Chevrolet','Corvette','Grand Sport 3LT Convertible RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 3LT Convertible RWD',98,10), +(2017,'Chevrolet','Corvette','Grand Sport 3LT Coupe RWD',98,16), +(2017,'Chevrolet','Corvette','Grand Sport 3LT Coupe RWD',98,10), +(2017,'Chevrolet','Corvette','Stingray',1,1), +(2017,'Chevrolet','Corvette','Stingray',1,2), +(2017,'Chevrolet','Corvette','Stingray 1LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 1LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray 1LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 1LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray 2LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 2LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray 2LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 2LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray 3LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 3LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray 3LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray 3LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51',1,1), +(2017,'Chevrolet','Corvette','Stingray Z51',1,2), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Convertible RWD',91,16), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Convertible RWD',91,10), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 1LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51 2LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 2LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51 2LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 2LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51 3LT Convertible RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 3LT Convertible RWD',90,10), +(2017,'Chevrolet','Corvette','Stingray Z51 3LT Coupe RWD',90,16), +(2017,'Chevrolet','Corvette','Stingray Z51 3LT Coupe RWD',90,10), +(2017,'Chevrolet','Corvette','Z06',1,1), +(2017,'Chevrolet','Corvette','Z06',1,2), +(2017,'Chevrolet','Corvette','Z06 1LZ Convertible RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 1LZ Convertible RWD',91,10), +(2017,'Chevrolet','Corvette','Z06 1LZ Coupe RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 1LZ Coupe RWD',91,10), +(2017,'Chevrolet','Corvette','Z06 2LZ Convertible RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 2LZ Convertible RWD',91,10), +(2017,'Chevrolet','Corvette','Z06 2LZ Coupe RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 2LZ Coupe RWD',91,10), +(2017,'Chevrolet','Corvette','Z06 3LZ Convertible RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 3LZ Convertible RWD',91,10), +(2017,'Chevrolet','Corvette','Z06 3LZ Coupe RWD',91,16), +(2017,'Chevrolet','Corvette','Z06 3LZ Coupe RWD',91,10), +(2017,'Chevrolet','Cruze','L',1,1), +(2017,'Chevrolet','Cruze','L',1,2), +(2017,'Chevrolet','Cruze','L Sedan FWD',67,9), +(2017,'Chevrolet','Cruze','LS',1,1), +(2017,'Chevrolet','Cruze','LS',1,2), +(2017,'Chevrolet','Cruze','LS Sedan FWD',67,7), +(2017,'Chevrolet','Cruze','LS Sedan FWD',67,9), +(2017,'Chevrolet','Cruze','LT',1,1), +(2017,'Chevrolet','Cruze','LT',1,2), +(2017,'Chevrolet','Cruze','LT Diesel Sedan FWD',67,7), +(2017,'Chevrolet','Cruze','LT Diesel Sedan FWD',99,9), +(2017,'Chevrolet','Cruze','LT Diesel Sedan FWD',99,4), +(2017,'Chevrolet','Cruze','LT Hatchback FWD',67,7), +(2017,'Chevrolet','Cruze','LT Hatchback FWD',67,9), +(2017,'Chevrolet','Cruze','LT Sedan FWD',67,7), +(2017,'Chevrolet','Cruze','LT Sedan FWD',67,9), +(2017,'Chevrolet','Cruze','Premier',1,1), +(2017,'Chevrolet','Cruze','Premier',1,2), +(2017,'Chevrolet','Cruze','Premier Hatchback FWD',67,7), +(2017,'Chevrolet','Cruze','Premier Sedan FWD',67,7), +(2017,'Chevrolet','Cruze','RS',1,1), +(2017,'Chevrolet','Cruze','RS',1,2), +(2017,'Chevrolet','Equinox','1LT',1,1), +(2017,'Chevrolet','Equinox','1LT',1,2), +(2017,'Chevrolet','Equinox','L',1,1), +(2017,'Chevrolet','Equinox','L',1,2), +(2017,'Chevrolet','Equinox','L FWD',70,7), +(2017,'Chevrolet','Equinox','LS',1,1), +(2017,'Chevrolet','Equinox','LS',1,2), +(2017,'Chevrolet','Equinox','LS AWD',70,7), +(2017,'Chevrolet','Equinox','LS FWD',70,7), +(2017,'Chevrolet','Equinox','LT AWD',70,7), +(2017,'Chevrolet','Equinox','LT AWD',100,7), +(2017,'Chevrolet','Equinox','LT FWD',70,7), +(2017,'Chevrolet','Equinox','Premier',1,1), +(2017,'Chevrolet','Equinox','Premier',1,2), +(2017,'Chevrolet','Equinox','Premier AWD',70,7), +(2017,'Chevrolet','Equinox','Premier AWD',100,7), +(2017,'Chevrolet','Equinox','Premier FWD',70,7), +(2017,'Chevrolet','Express','2500 LS RWD',96,7), +(2017,'Chevrolet','Express','2500 LS RWD',96,10), +(2017,'Chevrolet','Express','2500 LS RWD',101,7), +(2017,'Chevrolet','Express','2500 LS RWD',101,10), +(2017,'Chevrolet','Express','2500 LS RWD',102,7), +(2017,'Chevrolet','Express','2500 LS RWD',102,10), +(2017,'Chevrolet','Express','2500 LT RWD',96,7), +(2017,'Chevrolet','Express','2500 LT RWD',96,10), +(2017,'Chevrolet','Express','2500 LT RWD',101,7), +(2017,'Chevrolet','Express','2500 LT RWD',101,10), +(2017,'Chevrolet','Express','2500 LT RWD',102,7), +(2017,'Chevrolet','Express','2500 LT RWD',102,10), +(2017,'Chevrolet','Express','3500 LS Extended RWD',96,7), +(2017,'Chevrolet','Express','3500 LS Extended RWD',96,10), +(2017,'Chevrolet','Express','3500 LS Extended RWD',101,7), +(2017,'Chevrolet','Express','3500 LS Extended RWD',101,10), +(2017,'Chevrolet','Express','3500 LS Extended RWD',102,7), +(2017,'Chevrolet','Express','3500 LS Extended RWD',102,10), +(2017,'Chevrolet','Express','3500 LS RWD',96,7), +(2017,'Chevrolet','Express','3500 LS RWD',96,10), +(2017,'Chevrolet','Express','3500 LS RWD',101,7), +(2017,'Chevrolet','Express','3500 LS RWD',101,10), +(2017,'Chevrolet','Express','3500 LS RWD',102,7), +(2017,'Chevrolet','Express','3500 LS RWD',102,10), +(2017,'Chevrolet','Express','3500 LT Extended RWD',96,7), +(2017,'Chevrolet','Express','3500 LT Extended RWD',96,10), +(2017,'Chevrolet','Express','3500 LT Extended RWD',101,7), +(2017,'Chevrolet','Express','3500 LT Extended RWD',101,10), +(2017,'Chevrolet','Express','3500 LT Extended RWD',102,7), +(2017,'Chevrolet','Express','3500 LT Extended RWD',102,10), +(2017,'Chevrolet','Express','3500 LT RWD',96,7), +(2017,'Chevrolet','Express','3500 LT RWD',96,10), +(2017,'Chevrolet','Express','3500 LT RWD',101,7), +(2017,'Chevrolet','Express','3500 LT RWD',101,10), +(2017,'Chevrolet','Express','3500 LT RWD',102,7), +(2017,'Chevrolet','Express','3500 LT RWD',102,10), +(2017,'Chevrolet','Express 2500','LS',1,1), +(2017,'Chevrolet','Express 2500','LS',1,2), +(2017,'Chevrolet','Express 2500','LT',1,1), +(2017,'Chevrolet','Express 2500','LT',1,2), +(2017,'Chevrolet','Express 2500','Work Van',1,1), +(2017,'Chevrolet','Express 2500','Work Van',1,2), +(2017,'Chevrolet','Express 3500','LS',1,1), +(2017,'Chevrolet','Express 3500','LS',1,2), +(2017,'Chevrolet','Express 3500','LT',1,1), +(2017,'Chevrolet','Express 3500','LT',1,2), +(2017,'Chevrolet','Express 3500','Work Van',1,1), +(2017,'Chevrolet','Express 3500','Work Van',1,2), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',96,7), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',96,10), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',101,7), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',101,10), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',102,7), +(2017,'Chevrolet','Express Cargo','2500 Extended RWD',102,10), +(2017,'Chevrolet','Express Cargo','2500 RWD',96,7), +(2017,'Chevrolet','Express Cargo','2500 RWD',96,10), +(2017,'Chevrolet','Express Cargo','2500 RWD',101,7), +(2017,'Chevrolet','Express Cargo','2500 RWD',101,10), +(2017,'Chevrolet','Express Cargo','2500 RWD',102,7), +(2017,'Chevrolet','Express Cargo','2500 RWD',102,10), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',96,7), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',96,10), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',101,7), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',101,10), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',102,7), +(2017,'Chevrolet','Express Cargo','3500 Extended RWD',102,10), +(2017,'Chevrolet','Express Cargo','3500 RWD',96,7), +(2017,'Chevrolet','Express Cargo','3500 RWD',96,10), +(2017,'Chevrolet','Express Cargo','3500 RWD',101,7), +(2017,'Chevrolet','Express Cargo','3500 RWD',101,10), +(2017,'Chevrolet','Express Cargo','3500 RWD',102,7), +(2017,'Chevrolet','Express Cargo','3500 RWD',102,10), +(2017,'Chevrolet','Express Chassis','3500 139 Cutaway RWD',101,7), +(2017,'Chevrolet','Express Chassis','3500 159 Cutaway RWD',101,7), +(2017,'Chevrolet','Express Chassis','3500 177 Cutaway RWD',101,7), +(2017,'Chevrolet','Express Chassis','4500 159 Cutaway RWD',101,7), +(2017,'Chevrolet','Impala','1LS',1,1), +(2017,'Chevrolet','Impala','1LS',1,2), +(2017,'Chevrolet','Impala','1LT',1,1), +(2017,'Chevrolet','Impala','1LT',1,2), +(2017,'Chevrolet','Impala','CNG 2FL',1,1), +(2017,'Chevrolet','Impala','CNG 2FL',1,2), +(2017,'Chevrolet','Impala','CNG 3LT',1,1), +(2017,'Chevrolet','Impala','CNG 3LT',1,2), +(2017,'Chevrolet','Impala','LS CNG FWD',103,12), +(2017,'Chevrolet','Impala','LS CNG FWD',104,12), +(2017,'Chevrolet','Impala','LS FWD',68,7), +(2017,'Chevrolet','Impala','LS FWD',68,12), +(2017,'Chevrolet','Impala','LS FWD',104,7), +(2017,'Chevrolet','Impala','LS FWD',104,12), +(2017,'Chevrolet','Impala','LS Fleet FWD',68,7), +(2017,'Chevrolet','Impala','LS Fleet FWD',68,12), +(2017,'Chevrolet','Impala','LS Fleet FWD',104,7), +(2017,'Chevrolet','Impala','LS Fleet FWD',104,12), +(2017,'Chevrolet','Impala','LT CNG FWD',103,12), +(2017,'Chevrolet','Impala','LT CNG FWD',104,12), +(2017,'Chevrolet','Impala','LT FWD',68,7), +(2017,'Chevrolet','Impala','LT FWD',68,12), +(2017,'Chevrolet','Impala','LT FWD',104,7), +(2017,'Chevrolet','Impala','LT FWD',104,12), +(2017,'Chevrolet','Impala','Premier 2LZ',1,1), +(2017,'Chevrolet','Impala','Premier 2LZ',1,2), +(2017,'Chevrolet','Impala','Premier FWD',104,12), +(2017,'Chevrolet','Malibu','1LS',1,1), +(2017,'Chevrolet','Malibu','1LS',1,2), +(2017,'Chevrolet','Malibu','1LT',1,1), +(2017,'Chevrolet','Malibu','1LT',1,2), +(2017,'Chevrolet','Malibu','L',1,1), +(2017,'Chevrolet','Malibu','L',1,2), +(2017,'Chevrolet','Malibu','L FWD',105,7), +(2017,'Chevrolet','Malibu','L FWD',106,4), +(2017,'Chevrolet','Malibu','LS FWD',105,7), +(2017,'Chevrolet','Malibu','LS Fleet FWD',105,7), +(2017,'Chevrolet','Malibu','LT FWD',105,7), +(2017,'Chevrolet','Malibu','Premier',1,1), +(2017,'Chevrolet','Malibu','Premier',1,2), +(2017,'Chevrolet','Malibu','Premier FWD',106,4), +(2017,'Chevrolet','Malibu Hybrid','Base',1,1), +(2017,'Chevrolet','Malibu Hybrid','Base',1,2), +(2017,'Chevrolet','Malibu Hybrid','FWD',107,11), +(2017,'Chevrolet','SS','Base',1,1), +(2017,'Chevrolet','SS','Base',1,2), +(2017,'Chevrolet','SS','RWD',108,7), +(2017,'Chevrolet','SS','RWD',108,9), +(2017,'Chevrolet','Silverado 1500','1LT',1,1), +(2017,'Chevrolet','Silverado 1500','1LT',1,2), +(2017,'Chevrolet','Silverado 1500','1LZ',1,1), +(2017,'Chevrolet','Silverado 1500','1LZ',1,2), +(2017,'Chevrolet','Silverado 1500','2LT',1,1), +(2017,'Chevrolet','Silverado 1500','2LT',1,2), +(2017,'Chevrolet','Silverado 1500','2LZ',1,1), +(2017,'Chevrolet','Silverado 1500','2LZ',1,2), +(2017,'Chevrolet','Silverado 1500','Custom',1,1), +(2017,'Chevrolet','Silverado 1500','Custom',1,2), +(2017,'Chevrolet','Silverado 1500','Custom Double Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Custom Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Custom Double Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Custom Double Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','High Country',1,1), +(2017,'Chevrolet','Silverado 1500','High Country',1,2), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab LB 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab LB 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab LB RWD',110,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab LB RWD',85,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab RWD',110,10), +(2017,'Chevrolet','Silverado 1500','High Country Crew Cab RWD',85,10), +(2017,'Chevrolet','Silverado 1500','LS',1,1), +(2017,'Chevrolet','Silverado 1500','LS',1,2), +(2017,'Chevrolet','Silverado 1500','LS 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LS 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Crew Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Double Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LS Double Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LS Double Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LS LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LS LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LS LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LS LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LS RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LS RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT',1,1), +(2017,'Chevrolet','Silverado 1500','LT',1,2), +(2017,'Chevrolet','Silverado 1500','LT 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab 4WD',109,10), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab RWD',109,10), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Crew Cab RWD',110,10), +(2017,'Chevrolet','Silverado 1500','LT Double Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Double Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Double Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LT LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LT RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 Crew Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 Crew Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 Crew Cab LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 Double Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LT Z71 LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ',1,1), +(2017,'Chevrolet','Silverado 1500','LTZ',1,2), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab 4WD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB 4WD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB RWD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB RWD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab LB RWD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab RWD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab RWD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Crew Cab RWD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab 4WD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab RWD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab RWD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Double Cab RWD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Crew Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Crew Cab 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Crew Cab LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Crew Cab LB 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Crew Cab LB 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Double Cab 4WD',110,10), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Double Cab 4WD',85,12), +(2017,'Chevrolet','Silverado 1500','LTZ Z71 Double Cab 4WD',85,10), +(2017,'Chevrolet','Silverado 1500','WT',1,1), +(2017,'Chevrolet','Silverado 1500','WT',1,2), +(2017,'Chevrolet','Silverado 1500','Work Truck 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Crew Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Double Cab 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Double Cab 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Double Cab RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck Double Cab RWD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck LB 4WD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck LB 4WD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck LB RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck LB RWD',110,12), +(2017,'Chevrolet','Silverado 1500','Work Truck RWD',109,12), +(2017,'Chevrolet','Silverado 1500','Work Truck RWD',110,12), +(2017,'Chevrolet','Silverado 2500','High Country',1,1), +(2017,'Chevrolet','Silverado 2500','High Country',1,2), +(2017,'Chevrolet','Silverado 2500','LT',1,1), +(2017,'Chevrolet','Silverado 2500','LT',1,2), +(2017,'Chevrolet','Silverado 2500','LTZ',1,1), +(2017,'Chevrolet','Silverado 2500','LTZ',1,2), +(2017,'Chevrolet','Silverado 2500','WT',1,1), +(2017,'Chevrolet','Silverado 2500','WT',1,2), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','High Country Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT Double Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LT LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LT LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','LTZ Double Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck Double Cab RWD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck LB 4WD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck LB 4WD',112,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck LB RWD',111,7), +(2017,'Chevrolet','Silverado 2500HD','Work Truck LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500','High Country',1,1), +(2017,'Chevrolet','Silverado 3500','High Country',1,2), +(2017,'Chevrolet','Silverado 3500','LT',1,1), +(2017,'Chevrolet','Silverado 3500','LT',1,2), +(2017,'Chevrolet','Silverado 3500','LTZ',1,1), +(2017,'Chevrolet','Silverado 3500','LTZ',1,2), +(2017,'Chevrolet','Silverado 3500','WT',1,1), +(2017,'Chevrolet','Silverado 3500','WT',1,2), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','High Country Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LT LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','LTZ Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck Double Cab LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB DRW 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB DRW 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB DRW RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB DRW RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD','Work Truck LB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT LWB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT LWB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT LWB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT LWB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','LT RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck Crew Cab 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck Crew Cab 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck Crew Cab RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck Crew Cab RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck LWB 4WD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck LWB 4WD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck LWB RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck LWB RWD',112,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck RWD',111,7), +(2017,'Chevrolet','Silverado 3500HD Chassis','Work Truck RWD',112,7), +(2017,'Chevrolet','Sonic','LS',1,1), +(2017,'Chevrolet','Sonic','LS',1,2), +(2017,'Chevrolet','Sonic','LS Sedan FWD',113,17), +(2017,'Chevrolet','Sonic','LS Sedan FWD',113,7), +(2017,'Chevrolet','Sonic','LT',1,1), +(2017,'Chevrolet','Sonic','LT',1,2), +(2017,'Chevrolet','Sonic','LT Fleet Hatchback FWD',113,7), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',66,17), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',66,7), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',66,9), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',113,17), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',113,7), +(2017,'Chevrolet','Sonic','LT Hatchback FWD',113,9), +(2017,'Chevrolet','Sonic','LT Sedan FWD',66,7), +(2017,'Chevrolet','Sonic','LT Sedan FWD',66,9), +(2017,'Chevrolet','Sonic','LT Sedan FWD',113,7), +(2017,'Chevrolet','Sonic','LT Sedan FWD',113,9), +(2017,'Chevrolet','Sonic','Premier',1,1), +(2017,'Chevrolet','Sonic','Premier',1,2), +(2017,'Chevrolet','Sonic','Premier Hatchback FWD',66,7), +(2017,'Chevrolet','Sonic','Premier Hatchback FWD',66,9), +(2017,'Chevrolet','Sonic','Premier Sedan FWD',66,7), +(2017,'Chevrolet','Sonic','Premier Sedan FWD',66,9), +(2017,'Chevrolet','Spark','1LT',1,1), +(2017,'Chevrolet','Spark','1LT',1,2), +(2017,'Chevrolet','Spark','1LT FWD',114,17), +(2017,'Chevrolet','Spark','1LT FWD',114,15), +(2017,'Chevrolet','Spark','2LT',1,1), +(2017,'Chevrolet','Spark','2LT',1,2), +(2017,'Chevrolet','Spark','2LT FWD',114,17), +(2017,'Chevrolet','Spark','2LT FWD',114,15), +(2017,'Chevrolet','Spark','ACTIV',1,1), +(2017,'Chevrolet','Spark','ACTIV',1,2), +(2017,'Chevrolet','Spark','ACTIV FWD',114,17), +(2017,'Chevrolet','Spark','ACTIV FWD',114,15), +(2017,'Chevrolet','Spark','LS',1,1), +(2017,'Chevrolet','Spark','LS',1,2), +(2017,'Chevrolet','Spark','LS FWD',114,17), +(2017,'Chevrolet','Spark','LS FWD',114,15), +(2017,'Chevrolet','Suburban','1500 Fleet 4WD',115,7), +(2017,'Chevrolet','Suburban','1500 Fleet 4WD',115,12), +(2017,'Chevrolet','Suburban','1500 Fleet RWD',115,7), +(2017,'Chevrolet','Suburban','1500 Fleet RWD',115,12), +(2017,'Chevrolet','Suburban','1500 LS 4WD',115,7), +(2017,'Chevrolet','Suburban','1500 LS RWD',115,7), +(2017,'Chevrolet','Suburban','1500 LT 4WD',115,7), +(2017,'Chevrolet','Suburban','1500 LT RWD',115,7), +(2017,'Chevrolet','Suburban','1500 Premier 4WD',115,7), +(2017,'Chevrolet','Suburban','1500 Premier RWD',115,7), +(2017,'Chevrolet','Suburban','3500HD LS Fleet 4WD',115,7), +(2017,'Chevrolet','Suburban','3500HD LS Fleet 4WD',116,7), +(2017,'Chevrolet','Suburban','3500HD LT Fleet 4WD',115,7), +(2017,'Chevrolet','Suburban','3500HD LT Fleet 4WD',116,7), +(2017,'Chevrolet','Suburban','LS',1,1), +(2017,'Chevrolet','Suburban','LS',1,2), +(2017,'Chevrolet','Suburban','LT',1,1), +(2017,'Chevrolet','Suburban','LT',1,2), +(2017,'Chevrolet','Suburban','Premier',1,1), +(2017,'Chevrolet','Suburban','Premier',1,2), +(2017,'Chevrolet','Tahoe','Fleet 4WD',115,7), +(2017,'Chevrolet','Tahoe','Fleet RWD',115,7), +(2017,'Chevrolet','Tahoe','LS',1,1), +(2017,'Chevrolet','Tahoe','LS',1,2), +(2017,'Chevrolet','Tahoe','LS 4WD',115,7), +(2017,'Chevrolet','Tahoe','LS RWD',115,7), +(2017,'Chevrolet','Tahoe','LT',1,1), +(2017,'Chevrolet','Tahoe','LT',1,2), +(2017,'Chevrolet','Tahoe','LT 4WD',115,7), +(2017,'Chevrolet','Tahoe','LT RWD',115,7), +(2017,'Chevrolet','Tahoe','Police 4WD',115,7), +(2017,'Chevrolet','Tahoe','Police RWD',115,7), +(2017,'Chevrolet','Tahoe','Premier',1,1), +(2017,'Chevrolet','Tahoe','Premier',1,2), +(2017,'Chevrolet','Tahoe','Premier 4WD',115,7), +(2017,'Chevrolet','Tahoe','Premier RWD',115,7), +(2017,'Chevrolet','Tahoe','Special Service 4WD',115,7), +(2017,'Chevrolet','Traverse','1LT',1,1), +(2017,'Chevrolet','Traverse','1LT',1,2), +(2017,'Chevrolet','Traverse','1LT AWD',117,7), +(2017,'Chevrolet','Traverse','1LT FWD',117,7), +(2017,'Chevrolet','Traverse','2LT',1,1), +(2017,'Chevrolet','Traverse','2LT',1,2), +(2017,'Chevrolet','Traverse','2LT AWD',117,7), +(2017,'Chevrolet','Traverse','2LT FWD',117,7), +(2017,'Chevrolet','Traverse','Base LS',1,1), +(2017,'Chevrolet','Traverse','Base LS',1,2), +(2017,'Chevrolet','Traverse','LS',1,1), +(2017,'Chevrolet','Traverse','LS',1,2), +(2017,'Chevrolet','Traverse','LS AWD',117,7), +(2017,'Chevrolet','Traverse','LS Base FWD',117,7), +(2017,'Chevrolet','Traverse','LS FWD',117,7), +(2017,'Chevrolet','Traverse','Premier',1,1), +(2017,'Chevrolet','Traverse','Premier',1,2), +(2017,'Chevrolet','Traverse','Premier AWD',65,7), +(2017,'Chevrolet','Traverse','Premier FWD',65,7), +(2017,'Chevrolet','Trax','LS',1,1), +(2017,'Chevrolet','Trax','LS',1,2), +(2017,'Chevrolet','Trax','LS AWD',66,7), +(2017,'Chevrolet','Trax','LS FWD',66,7), +(2017,'Chevrolet','Trax','LT',1,1), +(2017,'Chevrolet','Trax','LT',1,2), +(2017,'Chevrolet','Trax','LT AWD',66,7), +(2017,'Chevrolet','Trax','LT FWD',66,7), +(2017,'Chevrolet','Trax','Premier',1,1), +(2017,'Chevrolet','Trax','Premier',1,2), +(2017,'Chevrolet','Trax','Premier AWD',66,7), +(2017,'Chevrolet','Trax','Premier FWD',66,7), +(2017,'Chevrolet','Volt','LT',1,1), +(2017,'Chevrolet','Volt','LT',1,2), +(2017,'Chevrolet','Volt','LT FWD',118,11), +(2017,'Chevrolet','Volt','Premier',1,1), +(2017,'Chevrolet','Volt','Premier',1,2), +(2017,'Chevrolet','Volt','Premier FWD',118,11), +(2017,'Chrysler','200','C',1,1), +(2017,'Chrysler','200','C',1,2), +(2017,'Chrysler','200','C Platinum Sedan AWD',119,4), +(2017,'Chrysler','200','C Platinum Sedan AWD',120,4), +(2017,'Chrysler','200','C Platinum Sedan FWD',119,4), +(2017,'Chrysler','200','C Platinum Sedan FWD',120,4), +(2017,'Chrysler','200','C Sedan AWD',119,4), +(2017,'Chrysler','200','C Sedan AWD',120,4), +(2017,'Chrysler','200','C Sedan FWD',119,4), +(2017,'Chrysler','200','C Sedan FWD',120,4), +(2017,'Chrysler','200','LX',1,1), +(2017,'Chrysler','200','LX',1,2), +(2017,'Chrysler','200','LX Sedan FWD',119,4), +(2017,'Chrysler','200','Limited',1,1), +(2017,'Chrysler','200','Limited',1,2), +(2017,'Chrysler','200','Limited Platinum Sedan FWD',119,4), +(2017,'Chrysler','200','Limited Platinum Sedan FWD',120,4), +(2017,'Chrysler','200','Limited Sedan FWD',119,4), +(2017,'Chrysler','200','Limited Sedan FWD',120,4), +(2017,'Chrysler','200','S',1,1), +(2017,'Chrysler','200','S',1,2), +(2017,'Chrysler','200','S Alloy Edition Sedan AWD',119,4), +(2017,'Chrysler','200','S Alloy Edition Sedan AWD',120,4), +(2017,'Chrysler','200','S Alloy Edition Sedan FWD',119,4), +(2017,'Chrysler','200','S Alloy Edition Sedan FWD',120,4), +(2017,'Chrysler','200','S Sedan AWD',119,4), +(2017,'Chrysler','200','S Sedan AWD',120,4), +(2017,'Chrysler','200','S Sedan FWD',119,4), +(2017,'Chrysler','200','S Sedan FWD',120,4), +(2017,'Chrysler','200','Touring Sedan FWD',119,4), +(2017,'Chrysler','300','C AWD',121,10), +(2017,'Chrysler','300','C Platinum AWD',121,10), +(2017,'Chrysler','300','C Platinum RWD',121,10), +(2017,'Chrysler','300','C Platinum RWD',122,10), +(2017,'Chrysler','300','C RWD',121,10), +(2017,'Chrysler','300','C RWD',122,10), +(2017,'Chrysler','300','Limited',1,1), +(2017,'Chrysler','300','Limited',1,2), +(2017,'Chrysler','300','Limited AWD',121,10), +(2017,'Chrysler','300','Limited RWD',121,10), +(2017,'Chrysler','300','S',1,1), +(2017,'Chrysler','300','S',1,2), +(2017,'Chrysler','300','S AWD',123,10), +(2017,'Chrysler','300','S Alloy Edition AWD',123,10), +(2017,'Chrysler','300','S Alloy Edition RWD',123,10), +(2017,'Chrysler','300','S Alloy Edition RWD',122,10), +(2017,'Chrysler','300','S RWD',123,10), +(2017,'Chrysler','300','S RWD',122,10), +(2017,'Chrysler','300','Touring AWD',123,10), +(2017,'Chrysler','300','Touring RWD',123,10), +(2017,'Chrysler','300C','Base',1,1), +(2017,'Chrysler','300C','Base',1,2), +(2017,'Chrysler','300C','Platinum',1,1), +(2017,'Chrysler','300C','Platinum',1,2), +(2017,'Chrysler','Pacifica','Base',1,1), +(2017,'Chrysler','Pacifica','Base',1,2), +(2017,'Chrysler','Pacifica','LX',1,1), +(2017,'Chrysler','Pacifica','LX',1,2), +(2017,'Chrysler','Pacifica','LX FWD',124,4), +(2017,'Chrysler','Pacifica','Limited',1,1), +(2017,'Chrysler','Pacifica','Limited',1,2), +(2017,'Chrysler','Pacifica','Limited FWD',124,4), +(2017,'Chrysler','Pacifica','Touring',1,1), +(2017,'Chrysler','Pacifica','Touring',1,2), +(2017,'Chrysler','Pacifica','Touring FWD',124,4), +(2017,'Chrysler','Pacifica','Touring L FWD',124,4), +(2017,'Chrysler','Pacifica','Touring L Plus FWD',124,4), +(2017,'Chrysler','Pacifica','Touring Plus FWD',124,4), +(2017,'Chrysler','Pacifica','Touring-L',1,1), +(2017,'Chrysler','Pacifica','Touring-L',1,2), +(2017,'Chrysler','Pacifica','Touring-L Plus',1,1), +(2017,'Chrysler','Pacifica','Touring-L Plus',1,2), +(2017,'Chrysler','Pacifica Hybrid','Limited FWD',125,11), +(2017,'Chrysler','Pacifica Hybrid','Platinum',1,1), +(2017,'Chrysler','Pacifica Hybrid','Platinum',1,2); diff --git a/data/vehicle-etl/reset_database.sh b/data/vehicle-etl/reset_database.sh new file mode 100755 index 0000000..892ccfa --- /dev/null +++ b/data/vehicle-etl/reset_database.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Reset vehicle database tables before a fresh import. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "==========================================" +echo "Vehicle Database Reset" +echo "==========================================" +echo "" + +# Check if postgres container is running +if ! docker ps --filter "name=mvp-postgres" --format "{{.Names}}" | grep -q "mvp-postgres"; then + echo "Error: mvp-postgres container is not running" + exit 1 +fi + +echo "Current data (before reset):" +docker exec mvp-postgres psql -U postgres -d motovaultpro -c \ + "SELECT + (SELECT COUNT(*) FROM engines) as engines, + (SELECT COUNT(*) FROM transmissions) as transmissions, + (SELECT COUNT(*) FROM vehicle_options) as vehicle_options;" 2>/dev/null || echo " Tables may not exist yet" +echo "" + +# Confirm reset +read -p "Are you sure you want to reset all vehicle data? (y/N) " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Reset cancelled." + exit 0 +fi + +echo "" +echo "Truncating tables..." +docker exec -i mvp-postgres psql -U postgres -d motovaultpro <<'EOF' +TRUNCATE TABLE vehicle_options RESTART IDENTITY CASCADE; +TRUNCATE TABLE engines RESTART IDENTITY CASCADE; +TRUNCATE TABLE transmissions RESTART IDENTITY CASCADE; +EOF + +echo "" +echo "==========================================" +echo "Reset complete" +echo "==========================================" +echo "" +echo "Verification (should all be 0):" +docker exec mvp-postgres psql -U postgres -d motovaultpro -c \ + "SELECT + (SELECT COUNT(*) FROM engines) as engines, + (SELECT COUNT(*) FROM transmissions) as transmissions, + (SELECT COUNT(*) FROM vehicle_options) as vehicle_options;" +echo "" +echo "Ready for fresh import with: ./import_data.sh" diff --git a/data/vehicle-etl/vehapi_fetch_snapshot.py b/data/vehicle-etl/vehapi_fetch_snapshot.py index 819eeeb..ce30044 100644 --- a/data/vehicle-etl/vehapi_fetch_snapshot.py +++ b/data/vehicle-etl/vehapi_fetch_snapshot.py @@ -32,7 +32,7 @@ except ImportError: # pragma: no cover - env guard SCRIPT_VERSION = "vehapi_fetch_snapshot.py@1.1.0" DEFAULT_MIN_YEAR = 2015 DEFAULT_MAX_YEAR = 2022 -DEFAULT_RATE_PER_SEC = 55 # stays under the 60 req/sec ceiling +DEFAULT_RATE_PER_MIN = 55 # stays under the 60 req/min ceiling MAX_ATTEMPTS = 5 FALLBACK_TRIMS = ["Base"] FALLBACK_TRANSMISSIONS = ["Manual", "Automatic"] @@ -95,22 +95,18 @@ def ensure_snapshot_dir(root: Path, custom_dir: Optional[str]) -> Path: class RateLimiter: - """Simple leaky bucket limiter to stay below the VehAPI threshold.""" + """Fixed delay limiter to stay below the VehAPI threshold (60 req/min).""" - def __init__(self, max_per_sec: int) -> None: - self.max_per_sec = max_per_sec - self._history: List[float] = [] + def __init__(self, max_per_min: int) -> None: + self.delay = 60.0 / max_per_min # ~1.09 sec for 55 rpm + self._last_request = 0.0 def acquire(self) -> None: - while True: - now = time.monotonic() - window_start = now - 1 - self._history = [ts for ts in self._history if ts >= window_start] - if len(self._history) < self.max_per_sec: - break - sleep_for = max(self._history[0] - window_start, 0.001) - time.sleep(sleep_for) - self._history.append(time.monotonic()) + now = time.monotonic() + elapsed = now - self._last_request + if elapsed < self.delay: + time.sleep(self.delay - elapsed) + self._last_request = time.monotonic() @dataclass @@ -132,7 +128,7 @@ class VehapiFetcher: allowed_makes: Sequence[str], snapshot_path: Path, responses_cache: bool = True, - rate_per_sec: int = DEFAULT_RATE_PER_SEC, + rate_per_min: int = DEFAULT_RATE_PER_MIN, ) -> None: self.session = session self.base_url = base_url.rstrip("/") @@ -146,7 +142,7 @@ class VehapiFetcher: self.conn.execute("PRAGMA synchronous=NORMAL;") self._init_schema() self.responses_cache = responses_cache - self.rate_limiter = RateLimiter(rate_per_sec) + self.rate_limiter = RateLimiter(rate_per_min) self.counts = FetchCounts() def _init_schema(self) -> None: @@ -251,7 +247,7 @@ class VehapiFetcher: retry_seconds = float(retry_after) except (TypeError, ValueError): retry_seconds = 30.0 - sleep_for = retry_seconds + random.uniform(0, 3) + sleep_for = retry_seconds + random.uniform(0, 0.5) print(f"[info] {label}: hit 429, sleeping {sleep_for:.1f}s before retry", file=sys.stderr) time.sleep(sleep_for) backoff = min(backoff * 2, 30) @@ -374,6 +370,7 @@ class VehapiFetcher: self._fetch_engines_for_transmission(year, make, model, trim, trans, trans_bucket) def _fetch_trims_for_model(self, year: int, make: str, model: str) -> None: + print(f" -> {year} {make} {model}", file=sys.stderr) path = ["trims", str(year), make, model] label = f"trims:{year}/{make}/{model}" trims_payload = self._request_json(path, label) @@ -416,9 +413,10 @@ class VehapiFetcher: print(f"[info] {year}: no allowed makes found, skipping", file=sys.stderr) continue print(f"[info] {year}: {len(makes)} makes", file=sys.stderr) - for make in makes: - print(f"[info] {year} {make}: fetching models", file=sys.stderr) + for idx, make in enumerate(makes, 1): + print(f"[{year}] ({idx}/{len(makes)}) {make}", file=sys.stderr) self._fetch_models_for_make(year, make) + print(f" [{self.counts.pairs_inserted} pairs so far]", file=sys.stderr) self.conn.commit() return self.counts @@ -429,7 +427,7 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument("--max-year", type=int, default=int(read_env("MAX_YEAR", DEFAULT_MAX_YEAR)), help="Inclusive max year (default env MAX_YEAR or 2026)") parser.add_argument("--snapshot-dir", type=str, help="Target snapshot directory (default snapshots/)") parser.add_argument("--base-url", type=str, default=read_env("VEHAPI_BASE_URL", DEFAULT_BASE_URL), help="VehAPI base URL (e.g. https://vehapi.com/api/v1/car-lists/get/car)") - parser.add_argument("--rate-per-sec", type=int, default=int(read_env("VEHAPI_MAX_RPS", DEFAULT_RATE_PER_SEC)), help="Max requests per second (<=60)") + parser.add_argument("--rate-per-min", type=int, default=int(read_env("VEHAPI_MAX_RPM", DEFAULT_RATE_PER_MIN)), help="Max requests per minute (<=60)") parser.add_argument("--makes-file", type=str, default="source-makes.txt", help="Path to source-makes.txt") parser.add_argument("--api-key-file", type=str, default="vehapi.key", help="Path to VehAPI bearer token file") parser.add_argument("--no-response-cache", action="store_true", help="Disable request cache stored in snapshot.sqlite") @@ -477,7 +475,7 @@ def main(argv: Sequence[str]) -> int: allowed_makes=allowed_makes, snapshot_path=snapshot_path, responses_cache=not args.no_response_cache, - rate_per_sec=args.rate_per_sec, + rate_per_min=args.rate_per_min, ) started_at = datetime.now(timezone.utc) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4cf68fb..63e9225 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -37,6 +37,11 @@ const DocumentsMobileScreen = lazy(() => import('./features/documents/mobile/Doc const AdminUsersPage = lazy(() => import('./pages/admin/AdminUsersPage').then(m => ({ default: m.AdminUsersPage }))); const AdminCatalogPage = lazy(() => import('./pages/admin/AdminCatalogPage').then(m => ({ default: m.AdminCatalogPage }))); const AdminStationsPage = lazy(() => import('./pages/admin/AdminStationsPage').then(m => ({ default: m.AdminStationsPage }))); + +// Admin mobile screens (lazy-loaded) +const AdminUsersMobileScreen = lazy(() => import('./features/admin/mobile/AdminUsersMobileScreen').then(m => ({ default: m.AdminUsersMobileScreen }))); +const AdminCatalogMobileScreen = lazy(() => import('./features/admin/mobile/AdminCatalogMobileScreen').then(m => ({ default: m.AdminCatalogMobileScreen }))); +const AdminStationsMobileScreen = lazy(() => import('./features/admin/mobile/AdminStationsMobileScreen').then(m => ({ default: m.AdminStationsMobileScreen }))); import { HomePage } from './pages/HomePage'; import { BottomNavigation, NavigationItem } from './shared-minimal/components/mobile/BottomNavigation'; import { GlassCard } from './shared-minimal/components/mobile/GlassCard'; @@ -604,6 +609,81 @@ function App() { )} + {activeScreen === "AdminUsers" && ( + + + + +
+
+ Loading admin users... +
+
+
+ + }> + +
+
+
+ )} + {activeScreen === "AdminCatalog" && ( + + + + +
+
+ Loading vehicle catalog... +
+
+
+ + }> + +
+
+
+ )} + {activeScreen === "AdminStations" && ( + + + + +
+
+ Loading station management... +
+
+
+ + }> + +
+
+
+ )} diff --git a/frontend/src/core/store/navigation.ts b/frontend/src/core/store/navigation.ts index acab18d..84d7bc4 100644 --- a/frontend/src/core/store/navigation.ts +++ b/frontend/src/core/store/navigation.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { safeStorage } from '../utils/safe-storage'; -export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Stations' | 'Documents' | 'Settings'; +export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Stations' | 'Documents' | 'Settings' | 'AdminUsers' | 'AdminCatalog' | 'AdminStations'; export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit'; interface NavigationHistory { diff --git a/frontend/src/features/admin/api/admin.api.ts b/frontend/src/features/admin/api/admin.api.ts index 5b1b94e..b6131bf 100644 --- a/frontend/src/features/admin/api/admin.api.ts +++ b/frontend/src/features/admin/api/admin.api.ts @@ -27,6 +27,10 @@ import { StationOverview, CreateStationRequest, UpdateStationRequest, + CatalogSearchResponse, + ImportPreviewResult, + ImportApplyResult, + CascadeDeleteResult, } from '../types/admin.types'; export interface AuditLogsResponse { @@ -194,4 +198,73 @@ export const adminApi = { deleteStation: async (id: string): Promise => { await apiClient.delete(`/admin/stations/${id}`); }, + + // Catalog Search + searchCatalog: async ( + query: string, + page: number = 1, + pageSize: number = 50 + ): Promise => { + const response = await apiClient.get('/admin/catalog/search', { + params: { q: query, page, pageSize }, + }); + return response.data; + }, + + // Catalog Import/Export + importPreview: async (file: File): Promise => { + const formData = new FormData(); + formData.append('file', file); + const response = await apiClient.post( + '/admin/catalog/import/preview', + formData, + { + headers: { 'Content-Type': 'multipart/form-data' }, + } + ); + return response.data; + }, + + importApply: async (previewId: string): Promise => { + const response = await apiClient.post('/admin/catalog/import/apply', { + previewId, + }); + return response.data; + }, + + exportCatalog: async (): Promise => { + const response = await apiClient.get('/admin/catalog/export', { + responseType: 'blob', + }); + return response.data; + }, + + // Cascade Delete + deleteMakeCascade: async (id: string): Promise => { + const response = await apiClient.delete( + `/admin/catalog/makes/${id}/cascade` + ); + return response.data; + }, + + deleteModelCascade: async (id: string): Promise => { + const response = await apiClient.delete( + `/admin/catalog/models/${id}/cascade` + ); + return response.data; + }, + + deleteYearCascade: async (id: string): Promise => { + const response = await apiClient.delete( + `/admin/catalog/years/${id}/cascade` + ); + return response.data; + }, + + deleteTrimCascade: async (id: string): Promise => { + const response = await apiClient.delete( + `/admin/catalog/trims/${id}/cascade` + ); + return response.data; + }, }; diff --git a/frontend/src/features/admin/hooks/useCatalog.ts b/frontend/src/features/admin/hooks/useCatalog.ts index b28af68..42403ee 100644 --- a/frontend/src/features/admin/hooks/useCatalog.ts +++ b/frontend/src/features/admin/hooks/useCatalog.ts @@ -316,3 +316,130 @@ export const useDeleteEngine = () => { }, }); }; + +// Catalog Search +export const useCatalogSearch = (query: string, page: number = 1, pageSize: number = 50) => { + const { isAuthenticated, isLoading } = useAuth0(); + + return useQuery({ + queryKey: ['catalogSearch', query, page, pageSize], + queryFn: () => adminApi.searchCatalog(query, page, pageSize), + enabled: isAuthenticated && !isLoading && query.length > 0, + staleTime: 30 * 1000, // 30 seconds - search results can change + gcTime: 5 * 60 * 1000, + retry: 1, + refetchOnWindowFocus: false, + }); +}; + +// Import/Export +export const useImportPreview = () => { + return useMutation({ + mutationFn: (file: File) => adminApi.importPreview(file), + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to preview import'); + }, + }); +}; + +export const useImportApply = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (previewId: string) => adminApi.importApply(previewId), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['catalogSearch'] }); + toast.success( + `Import completed: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted` + ); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to apply import'); + }, + }); +}; + +export const useExportCatalog = () => { + return useMutation({ + mutationFn: () => adminApi.exportCatalog(), + onSuccess: (blob) => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'vehicle-catalog.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + toast.success('Catalog exported successfully'); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to export catalog'); + }, + }); +}; + +// Cascade Delete +export const useDeleteMakeCascade = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => adminApi.deleteMakeCascade(id), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['catalogMakes'] }); + queryClient.invalidateQueries({ queryKey: ['catalogSearch'] }); + toast.success(`Deleted ${result.totalDeleted} items (cascade)`); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to cascade delete make'); + }, + }); +}; + +export const useDeleteModelCascade = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => adminApi.deleteModelCascade(id), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['catalogModels'] }); + queryClient.invalidateQueries({ queryKey: ['catalogSearch'] }); + toast.success(`Deleted ${result.totalDeleted} items (cascade)`); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to cascade delete model'); + }, + }); +}; + +export const useDeleteYearCascade = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => adminApi.deleteYearCascade(id), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['catalogYears'] }); + queryClient.invalidateQueries({ queryKey: ['catalogSearch'] }); + toast.success(`Deleted ${result.totalDeleted} items (cascade)`); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to cascade delete year'); + }, + }); +}; + +export const useDeleteTrimCascade = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => adminApi.deleteTrimCascade(id), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['catalogTrims'] }); + queryClient.invalidateQueries({ queryKey: ['catalogSearch'] }); + toast.success(`Deleted ${result.totalDeleted} items (cascade)`); + }, + onError: (error: ApiError) => { + toast.error(error.response?.data?.error || 'Failed to cascade delete trim'); + }, + }); +}; diff --git a/frontend/src/features/admin/mobile/AdminCatalogMobileScreen.tsx b/frontend/src/features/admin/mobile/AdminCatalogMobileScreen.tsx index 92faf35..5a53e62 100644 --- a/frontend/src/features/admin/mobile/AdminCatalogMobileScreen.tsx +++ b/frontend/src/features/admin/mobile/AdminCatalogMobileScreen.tsx @@ -1,642 +1,171 @@ -import React, { useMemo, useState } from 'react'; +/** + * @ai-summary Admin Vehicle Catalog mobile screen with search-first UI + * @ai-context Uses server-side search and pagination, optimized for touch + */ + +import React, { useState, useCallback, useRef } from 'react'; import { Navigate } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; +import { + Search, + FileDownload, + FileUpload, + Delete, + MoreVert, + Close, + History, +} from '@mui/icons-material'; import toast from 'react-hot-toast'; -import { History } from '@mui/icons-material'; import { useAdminAccess } from '../../../core/auth/useAdminAccess'; -import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; import { AuditLogDrawer } from '../components/AuditLogDrawer'; -import { useBulkSelection } from '../hooks/useBulkSelection'; import { - useMakes, - useCreateMake, - useUpdateMake, - useDeleteMake, - useModels, - useCreateModel, - useUpdateModel, - useDeleteModel, - useYears, - useCreateYear, - useDeleteYear, - useTrims, - useCreateTrim, - useUpdateTrim, - useDeleteTrim, - useEngines, - useCreateEngine, - useUpdateEngine, - useDeleteEngine, + useCatalogSearch, + useExportCatalog, + useImportPreview, + useImportApply, } from '../hooks/useCatalog'; +import { adminApi } from '../api/admin.api'; import { - CatalogLevel, - CatalogRow, - CatalogSelectionContext, - LEVEL_LABEL, - LEVEL_SINGULAR_LABEL, - NEXT_LEVEL, - getCascadeSummary, -} from '../catalog/catalogShared'; -import { - CatalogFormValues, - buildDefaultValues, -} from '../catalog/catalogSchemas'; -import { - CatalogEngine, - CatalogMake, - CatalogModel, - CatalogTrim, - CatalogYear, + CatalogSearchResult, + ImportPreviewResult, } from '../types/admin.types'; -interface BreadcrumbItem { - label: string; - context: CatalogSelectionContext; -} - -const getCardChildSummary = ( - level: CatalogLevel, - item: CatalogRow, - modelsByMake: Map, - yearsByModel: Map, - trimsByYear: Map, - enginesByTrim: Map -): string | null => { - switch (level) { - case 'makes': { - const count = modelsByMake.get(item.id)?.length ?? 0; - return `${count} ${count === 1 ? 'model' : 'models'}`; - } - case 'models': { - const count = yearsByModel.get(item.id)?.length ?? 0; - return `${count} ${count === 1 ? 'year' : 'years'}`; - } - case 'years': { - const count = trimsByYear.get(item.id)?.length ?? 0; - return `${count} ${count === 1 ? 'trim' : 'trims'}`; - } - case 'trims': { - const count = enginesByTrim.get(item.id)?.length ?? 0; - return `${count} ${count === 1 ? 'engine' : 'engines'}`; - } - default: - return null; - } -}; - -const getRowDisplayName = (level: CatalogLevel, row: CatalogRow): string => { - if (level === 'years') { - return String((row as CatalogYear).year); - } - return ( - row as CatalogMake | CatalogModel | CatalogTrim | CatalogEngine - ).name; -}; - -const normalizeCollection = (value: unknown, key: string): T[] => { - if (Array.isArray(value)) { - return value as T[]; - } - if ( - value && - typeof value === 'object' && - Array.isArray((value as Record)[key]) - ) { - return (value as Record)[key]; - } - return []; -}; - export const AdminCatalogMobileScreen: React.FC = () => { - const { isAdmin, loading: authLoading } = useAdminAccess(); - const queryClient = useQueryClient(); + const { loading: authLoading, isAdmin } = useAdminAccess(); - const [selection, setSelection] = useState({ - level: 'makes', - }); - const [multiSelectMode, setMultiSelectMode] = useState(false); - const [bulkSheetOpen, setBulkSheetOpen] = useState(false); + // Search state + const [searchInput, setSearchInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [page, setPage] = useState(1); + const pageSize = 25; + + // UI state + const [menuOpen, setMenuOpen] = useState(false); const [auditDrawerOpen, setAuditDrawerOpen] = useState(false); - const [formState, setFormState] = useState<{ + + // Delete state + const [deleteSheet, setDeleteSheet] = useState<{ open: boolean; - mode: 'create' | 'edit'; - entity?: CatalogRow; - data: CatalogFormValues; - }>({ - open: false, - mode: 'create', - entity: undefined, - data: {}, - }); + item: CatalogSearchResult | null; + }>({ open: false, item: null }); + const [deleting, setDeleting] = useState(false); - const makesQuery = useMakes(); - const modelsQuery = useModels(selection.make?.id); - const yearsQuery = useYears(selection.model?.id); - const trimsQuery = useTrims(selection.year?.id); - const enginesQuery = useEngines(selection.trim?.id); + // Import state + const [importSheet, setImportSheet] = useState(false); + const [importPreview, setImportPreview] = useState(null); + const fileInputRef = useRef(null); - const makes = normalizeCollection(makesQuery.data, 'makes'); - const models = selection.make - ? normalizeCollection(modelsQuery.data, 'models') - : []; - const years = selection.model - ? normalizeCollection(yearsQuery.data, 'years') - : []; - const trims = selection.year - ? normalizeCollection(trimsQuery.data, 'trims') - : []; - const engines = selection.trim - ? normalizeCollection(enginesQuery.data, 'engines') - : []; + // Hooks + const { data: searchData, isLoading: searchLoading, refetch } = useCatalogSearch( + searchQuery, + page, + pageSize + ); + const exportMutation = useExportCatalog(); + const importPreviewMutation = useImportPreview(); + const importApplyMutation = useImportApply(); - const createMake = useCreateMake(); - const updateMake = useUpdateMake(); - const deleteMake = useDeleteMake(); + // Search handlers + const handleSearch = useCallback(() => { + setPage(1); + setSearchQuery(searchInput); + }, [searchInput]); - const createModel = useCreateModel(); - const updateModel = useUpdateModel(); - const deleteModel = useDeleteModel(); - - const createYear = useCreateYear(); - const deleteYear = useDeleteYear(); - - const createTrim = useCreateTrim(); - const updateTrim = useUpdateTrim(); - const deleteTrim = useDeleteTrim(); - - const createEngine = useCreateEngine(); - const updateEngine = useUpdateEngine(); - const deleteEngine = useDeleteEngine(); - - const modelsByMake = useMemo(() => { - const map = new Map(); - models.forEach((model) => { - const list = map.get(model.makeId) ?? []; - list.push(model); - map.set(model.makeId, list); - }); - return map; - }, [models]); - - const yearsByModel = useMemo(() => { - const map = new Map(); - years.forEach((year) => { - const list = map.get(year.modelId) ?? []; - list.push(year); - map.set(year.modelId, list); - }); - return map; - }, [years]); - - const trimsByYear = useMemo(() => { - const map = new Map(); - trims.forEach((trim) => { - const list = map.get(trim.yearId) ?? []; - list.push(trim); - map.set(trim.yearId, list); - }); - return map; - }, [trims]); - - const enginesByTrim = useMemo(() => { - const map = new Map(); - engines.forEach((engine) => { - const list = map.get(engine.trimId) ?? []; - list.push(engine); - map.set(engine.trimId, list); - }); - return map; - }, [engines]); - - const currentData = useMemo(() => { - switch (selection.level) { - case 'makes': - return makes; - case 'models': - return selection.make ? models : []; - case 'years': - return selection.model ? years : []; - case 'trims': - return selection.year ? trims : []; - case 'engines': - return selection.trim ? engines : []; - default: - return []; - } - }, [selection, makes, models, years, trims, engines]); - - const { - selected, - toggleItem, - reset: resetSelection, - count: selectedCount, - selectedItems, - } = useBulkSelection({ - items: currentData, - keyExtractor: (item) => item.id, - }); - - const cascadeSummary = useMemo( - () => - getCascadeSummary( - selection.level, - selectedItems, - modelsByMake, - yearsByModel, - trimsByYear, - enginesByTrim - ), - [ - selection.level, - selectedItems, - modelsByMake, - yearsByModel, - trimsByYear, - enginesByTrim, - ] + const handleSearchKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + handleSearch(); + } + }, + [handleSearch] ); - const breadcrumbs = useMemo(() => { - const crumbs: BreadcrumbItem[] = [ - { label: 'Catalog', context: { level: 'makes' } }, - ]; + const handleClearSearch = useCallback(() => { + setSearchInput(''); + setSearchQuery(''); + setPage(1); + }, []); - if (selection.make) { - crumbs.push({ label: 'Makes', context: { level: 'makes' } }); - crumbs.push({ - label: selection.make.name, - context: { - level: 'models', - make: selection.make, - }, - }); - } + // Pagination + const handleLoadMore = useCallback(() => { + setPage((prev) => prev + 1); + }, []); - if (selection.model) { - crumbs.push({ - label: 'Models', - context: { - level: 'models', - make: selection.make, - }, - }); - crumbs.push({ - label: selection.model.name, - context: { - level: 'years', - make: selection.make, - model: selection.model, - }, - }); - } + // Delete handlers + const handleDeleteClick = useCallback((item: CatalogSearchResult) => { + setDeleteSheet({ open: true, item }); + setMenuOpen(false); + }, []); - if (selection.year) { - crumbs.push({ - label: 'Years', - context: { - level: 'years', - make: selection.make, - model: selection.model, - year: selection.year, - }, - }); - crumbs.push({ - label: String(selection.year.year), - context: { - level: 'trims', - make: selection.make, - model: selection.model, - year: selection.year, - }, - }); - } - - if (selection.trim) { - crumbs.push({ - label: 'Trims', - context: { - level: 'trims', - make: selection.make, - model: selection.model, - year: selection.year, - trim: selection.trim, - }, - }); - crumbs.push({ - label: selection.trim.name, - context: { - level: 'engines', - make: selection.make, - model: selection.model, - year: selection.year, - trim: selection.trim, - }, - }); - } - - const currentLabel = LEVEL_LABEL[selection.level]; - if (!crumbs.some((crumb) => crumb.label === currentLabel)) { - crumbs.push({ label: currentLabel, context: selection }); - } - - return crumbs; - }, [selection]); - - const isLoading = - makesQuery.isLoading || - modelsQuery.isLoading || - yearsQuery.isLoading || - trimsQuery.isLoading || - enginesQuery.isLoading; - - const openCreateForm = () => { - setFormState({ - open: true, - mode: 'create', - entity: undefined, - data: buildDefaultValues(selection.level, 'create', undefined, selection), - }); - }; - - const openEditForm = (item: CatalogRow) => { - setFormState({ - open: true, - mode: 'edit', - entity: item, - data: buildDefaultValues(selection.level, 'edit', item, selection), - }); - }; - - const closeForm = () => { - setFormState({ - open: false, - mode: 'create', - entity: undefined, - data: {}, - }); - }; - - const updateFormField = (field: keyof CatalogFormValues, value: string | number | undefined) => { - setFormState((prev) => ({ - ...prev, - data: { - ...prev.data, - [field]: value, - }, - })); - }; - - const invalidateCatalogQueries = () => { - queryClient.invalidateQueries({ queryKey: ['catalogMakes'] }); - queryClient.invalidateQueries({ queryKey: ['catalogModels'] }); - queryClient.invalidateQueries({ queryKey: ['catalogYears'] }); - queryClient.invalidateQueries({ queryKey: ['catalogTrims'] }); - queryClient.invalidateQueries({ queryKey: ['catalogEngines'] }); - }; - - const handleFormSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - const data = formState.data; + const handleDeleteConfirm = useCallback(async () => { + if (!deleteSheet.item) return; + setDeleting(true); try { - if (formState.mode === 'create') { - switch (selection.level) { - case 'makes': - await createMake.mutateAsync({ - name: data.name?.trim() ?? '', - }); - break; - case 'models': - if (!selection.make) { - toast.error('Select a make before adding models'); - return; - } - await createModel.mutateAsync({ - name: data.name?.trim() ?? '', - makeId: selection.make.id, - }); - break; - case 'years': - if (!selection.model) { - toast.error('Select a model before adding years'); - return; - } - await createYear.mutateAsync({ - modelId: selection.model.id, - year: Number(data.year), - }); - break; - case 'trims': - if (!selection.year) { - toast.error('Select a year before adding trims'); - return; - } - await createTrim.mutateAsync({ - name: data.name?.trim() ?? '', - yearId: selection.year.id, - }); - break; - case 'engines': - if (!selection.trim) { - toast.error('Select a trim before adding engines'); - return; - } - await createEngine.mutateAsync({ - name: data.name?.trim() ?? '', - trimId: selection.trim.id, - displacement: data.displacement, - cylinders: - data.cylinders === undefined || data.cylinders === null - ? undefined - : Number(data.cylinders), - fuel_type: data.fuel_type, - }); - break; - } - } else if (formState.entity) { - const entityId = formState.entity.id; - switch (selection.level) { - case 'makes': - await updateMake.mutateAsync({ - id: entityId, - data: { name: data.name?.trim() ?? '' }, - }); - break; - case 'models': - await updateModel.mutateAsync({ - id: entityId, - data: { name: data.name?.trim() ?? '' }, - }); - break; - case 'trims': - await updateTrim.mutateAsync({ - id: entityId, - data: { name: data.name?.trim() ?? '' }, - }); - break; - case 'engines': - await updateEngine.mutateAsync({ - id: entityId, - data: { - name: data.name?.trim(), - displacement: data.displacement, - cylinders: - data.cylinders === undefined || data.cylinders === null - ? undefined - : Number(data.cylinders), - fuel_type: data.fuel_type, - }, - }); - break; - default: - break; - } - } - - closeForm(); - resetSelection(); + await adminApi.deleteEngine(deleteSheet.item.id.toString()); + toast.success('Configuration deleted'); + setDeleteSheet({ open: false, item: null }); + refetch(); } catch { - // Errors surfaced through toast in mutation hooks. + toast.error('Failed to delete configuration'); + } finally { + setDeleting(false); } - }; + }, [deleteSheet.item, refetch]); - const handleDeleteSingle = async (id: string) => { - const confirmed = window.confirm( - `Delete this ${LEVEL_SINGULAR_LABEL[selection.level]}?` - ); - if (!confirmed) return; + // Import handlers + const handleImportClick = useCallback(() => { + setMenuOpen(false); + fileInputRef.current?.click(); + }, []); - try { - switch (selection.level) { - case 'makes': - await deleteMake.mutateAsync(id); - break; - case 'models': - await deleteModel.mutateAsync(id); - break; - case 'years': - await deleteYear.mutateAsync(id); - break; - case 'trims': - await deleteTrim.mutateAsync(id); - break; - case 'engines': - await deleteEngine.mutateAsync(id); - break; - default: - break; - } - invalidateCatalogQueries(); - resetSelection(); - } catch { - // Mutation hooks notify user. - } - }; + const handleFileSelect = useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; - const handleBulkDelete = async () => { - if (selectedCount === 0) return; - setBulkSheetOpen(false); - - const ids = Array.from(selected); - let deleted = 0; - let failed = 0; - - for (const id of ids) { try { - switch (selection.level) { - case 'makes': - await deleteMake.mutateAsync(id); - break; - case 'models': - await deleteModel.mutateAsync(id); - break; - case 'years': - await deleteYear.mutateAsync(id); - break; - case 'trims': - await deleteTrim.mutateAsync(id); - break; - case 'engines': - await deleteEngine.mutateAsync(id); - break; - default: - break; - } - deleted += 1; + const result = await importPreviewMutation.mutateAsync(file); + setImportPreview(result); + setImportSheet(true); } catch { - failed += 1; + // Error handled by mutation } + + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, + [importPreviewMutation] + ); + + const handleImportConfirm = useCallback(async () => { + if (!importPreview?.previewId) return; + + try { + await importApplyMutation.mutateAsync(importPreview.previewId); + setImportSheet(false); + setImportPreview(null); + refetch(); + } catch { + // Error handled by mutation } + }, [importPreview, importApplyMutation, refetch]); - invalidateCatalogQueries(); - setMultiSelectMode(false); - resetSelection(); - - if (failed > 0) { - toast.error( - `Deleted ${deleted} ${LEVEL_LABEL[selection.level].toLowerCase()}, failed ${failed}` - ); - } else { - toast.success( - `Deleted ${deleted} ${LEVEL_LABEL[selection.level].toLowerCase()}` - ); - } - }; - - const drillDown = (item: CatalogRow) => { - if (multiSelectMode) return; - - switch (selection.level) { - case 'makes': - setSelection({ level: 'models', make: item as CatalogMake }); - resetSelection(); - break; - case 'models': - setSelection({ - level: 'years', - make: selection.make, - model: item as CatalogModel, - }); - resetSelection(); - break; - case 'years': - setSelection({ - level: 'trims', - make: selection.make, - model: selection.model, - year: item as CatalogYear, - }); - resetSelection(); - break; - case 'trims': - setSelection({ - level: 'engines', - make: selection.make, - model: selection.model, - year: selection.year, - trim: item as CatalogTrim, - }); - resetSelection(); - break; - default: - break; - } - }; - - const handleBreadcrumbClick = (crumb: BreadcrumbItem, index: number) => { - if (index === breadcrumbs.length - 1) return; - setSelection(crumb.context); - setMultiSelectMode(false); - resetSelection(); - }; + // Export handler + const handleExport = useCallback(() => { + setMenuOpen(false); + exportMutation.mutate(); + }, [exportMutation]); + // Auth loading if (authLoading) { return (
-
Loading admin access…
+
Loading admin access...
@@ -644,376 +173,364 @@ export const AdminCatalogMobileScreen: React.FC = () => { ); } + // Not admin if (!isAdmin) { return ; } - const currentLevelLabel = LEVEL_LABEL[selection.level]; - const nextLevel = NEXT_LEVEL[selection.level]; + const items = searchData?.items || []; + const total = searchData?.total || 0; + const hasMore = items.length < total; + const hasResults = searchQuery.length > 0 && items.length > 0; + const noResults = searchQuery.length > 0 && items.length === 0 && !searchLoading; return (
-
-
-
-

Vehicle Catalog

-

{currentLevelLabel}

-
-
- - -
-
- -
- {breadcrumbs.map((crumb, index) => ( - - {index > 0 && /} - - - ))} -
- -
+ {/* Header */} +
+
+

Vehicle Catalog

- {currentData.length} {currentLevelLabel.toLowerCase()} + {searchQuery ? `${total} results` : 'Search to view vehicles'}

- {currentData.length > 0 && ( - - )} +
+
+ +
- {isLoading && ( -
-
-
- )} - - {!isLoading && currentData.length === 0 && ( - -
-

No {currentLevelLabel.toLowerCase()} found

+ {/* Search Bar */} +
+
+ + setSearchInput(e.target.value)} + onKeyDown={handleSearchKeyDown} + className="w-full pl-10 pr-10 py-3 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500" + style={{ minHeight: '44px' }} + /> + {searchInput && ( + )} +
+ +
+ + {/* Instructions when no search */} + {!searchQuery && ( + +
+ +

Search for Vehicles

+

+ Enter a search term like "2024 Toyota Camry" to find vehicles in the catalog. +

+

+ Use the menu for import/export options. +

)} - {!isLoading && currentData.length > 0 && ( + {/* No Results */} + {noResults && ( + +
+

No Results Found

+

+ No vehicles match "{searchQuery}". Try a different search term. +

+
+
+ )} + + {/* Search Results */} + {hasResults && (
- {currentData.map((item) => { - const isSelected = selected.has(item.id); - const displayName = getRowDisplayName(selection.level, item); - const childSummary = getCardChildSummary( - selection.level, - item, - modelsByMake, - yearsByModel, - trimsByYear, - enginesByTrim - ); - - return ( - { - if (multiSelectMode) { - toggleItem(item.id); - } else { - drillDown(item); - } - }} - > -
- {multiSelectMode && ( - toggleItem(item.id)} - className="mt-1" - style={{ minWidth: '20px', minHeight: '20px' }} - /> - )} -
-

- {displayName} -

-
- - Created: {new Date(item.createdAt).toLocaleDateString()} + {items.map((item) => ( + +
+
+

+ {item.year} {item.make} {item.model} +

+

{item.trim}

+
+ {item.engineName && ( + + {item.engineName} - | - - Updated: {new Date(item.updatedAt).toLocaleDateString()} - -
- {childSummary && ( -

{childSummary}

)} - - {!multiSelectMode && ( -
- {selection.level !== 'years' && selection.level !== 'engines' && ( - - )} - {selection.level === 'engines' && ( - - )} - - {nextLevel && ( - - )} -
+ {item.transmissionType && ( + + {item.transmissionType} + )}
-
- ); - })} + +
+ + ))} + + {/* Load More */} + {hasMore && ( + + )}
)} + + {/* Hidden file input */} +
- {multiSelectMode && selectedCount > 0 && ( -
-
- Selected: {selectedCount} -
+ {/* Menu Sheet */} + {menuOpen && ( +
+
e.stopPropagation()} + > +
+

Options

-
+ + + + + +
)} - {formState.open && ( + {/* Delete Confirmation Sheet */} + {deleteSheet.open && deleteSheet.item && (
-

- {formState.mode === 'edit' - ? `Edit ${LEVEL_SINGULAR_LABEL[selection.level]}` - : `Create ${LEVEL_SINGULAR_LABEL[selection.level]}`} -

-
- {selection.level === 'years' ? ( - - updateFormField( - 'year', - e.target.value === '' ? undefined : Number(e.target.value) - ) - } - className="w-full border border-slate-300 rounded-lg px-4 py-3 text-slate-800" - style={{ minHeight: '44px' }} - required - /> - ) : ( - updateFormField('name', e.target.value)} - className="w-full border border-slate-300 rounded-lg px-4 py-3 text-slate-800" - style={{ minHeight: '44px' }} - required - /> - )} - - {selection.level === 'engines' && ( - <> - updateFormField('displacement', e.target.value)} - className="w-full border border-slate-300 rounded-lg px-4 py-3 text-slate-800" - style={{ minHeight: '44px' }} - /> - - updateFormField( - 'cylinders', - e.target.value === '' ? undefined : Number(e.target.value) - ) - } - className="w-full border border-slate-300 rounded-lg px-4 py-3 text-slate-800" - style={{ minHeight: '44px' }} - /> - updateFormField('fuel_type', e.target.value)} - className="w-full border border-slate-300 rounded-lg px-4 py-3 text-slate-800" - style={{ minHeight: '44px' }} - /> - - )} - -
- - -
-
-
-
- )} - - {bulkSheetOpen && ( -
-
-

- Delete {selectedCount} {currentLevelLabel.toLowerCase()}? -

- {cascadeSummary ? ( -

{cascadeSummary}

- ) : ( -

- This action cannot be undone. Dependent items will also be deleted. -

- )} -
-
    - {selectedItems.map((item) => ( -
  • - {getRowDisplayName(selection.level, item)} -
  • - ))} -
-
+

Delete Configuration?

+

+ Are you sure you want to delete{' '} + + {deleteSheet.item.year} {deleteSheet.item.make} {deleteSheet.item.model}{' '} + {deleteSheet.item.trim} + + ? +

)} + {/* Import Preview Sheet */} + {importSheet && importPreview && ( +
+
+
+

Import Preview

+ +
+ + {/* Summary */} +
+
+ {importPreview.toCreate.length} to create +
+
+ {importPreview.toUpdate.length} to update +
+
+ {importPreview.toDelete.length} to delete +
+
+ + {/* Errors */} + {importPreview.errors.length > 0 && ( +
+

+ {importPreview.errors.length} Error(s) Found: +

+
    + {importPreview.errors.slice(0, 5).map((err, idx) => ( +
  • + Row {err.row}: {err.error} +
  • + ))} + {importPreview.errors.length > 5 && ( +
  • ...and {importPreview.errors.length - 5} more errors
  • + )} +
+
+ )} + + {/* Status */} + {importPreview.valid ? ( +
+

+ The import file is valid and ready to be applied. +

+
+ ) : ( +
+

+ Please fix the errors above before importing. +

+
+ )} + +
+ + +
+
+
+ )} + + {/* Audit Log Drawer */} setAuditDrawerOpen(false)} diff --git a/frontend/src/features/admin/types/admin.types.ts b/frontend/src/features/admin/types/admin.types.ts index 9faa60b..ad5ab9c 100644 --- a/frontend/src/features/admin/types/admin.types.ts +++ b/frontend/src/features/admin/types/admin.types.ts @@ -158,3 +158,65 @@ export interface AdminAccessResponse { isAdmin: boolean; adminRecord: AdminUser | null; } + +// Catalog search types +export interface CatalogSearchResult { + id: number; + year: number; + make: string; + model: string; + trim: string; + engineId: number | null; + engineName: string | null; + transmissionId: number | null; + transmissionType: string | null; +} + +export interface CatalogSearchResponse { + items: CatalogSearchResult[]; + total: number; + page: number; + pageSize: number; +} + +// Catalog import types +export interface ImportRow { + action: 'add' | 'update' | 'delete'; + year: number; + make: string; + model: string; + trim: string; + engineName: string | null; + transmissionType: string | null; +} + +export interface ImportError { + row: number; + error: string; +} + +export interface ImportPreviewResult { + previewId: string; + toCreate: ImportRow[]; + toUpdate: ImportRow[]; + toDelete: ImportRow[]; + errors: ImportError[]; + valid: boolean; +} + +export interface ImportApplyResult { + created: number; + updated: number; + deleted: number; + errors: ImportError[]; +} + +// Cascade delete result +export interface CascadeDeleteResult { + deletedMakes: number; + deletedModels: number; + deletedYears: number; + deletedTrims: number; + deletedEngines: number; + totalDeleted: number; +} diff --git a/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx b/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx index 23bd7e0..f816f38 100644 --- a/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx +++ b/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; -import { useNavigate } from 'react-router-dom'; import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer'; import { useSettings } from '../hooks/useSettings'; import { useAdminAccess } from '../../../core/auth/useAdminAccess'; +import { useNavigationStore } from '../../../core/store'; interface ToggleSwitchProps { enabled: boolean; @@ -71,7 +71,7 @@ const Modal: React.FC = ({ isOpen, onClose, title, children }) => { export const MobileSettingsScreen: React.FC = () => { const { user, logout } = useAuth0(); - const navigate = useNavigate(); + const { navigateToScreen } = useNavigationStore(); const { settings, updateSetting, isLoading, error } = useSettings(); const { isAdmin, loading: adminLoading } = useAdminAccess(); const [showDataExport, setShowDataExport] = useState(false); @@ -258,7 +258,7 @@ export const MobileSettingsScreen: React.FC = () => {

Admin Console

+ + {/* Action Buttons */} + + + + + + + + + + + {/* Hidden file input for import */} + + + + {/* Instructions when no search */} + {!searchQuery && ( + + + Search for Vehicles + + + Enter a search term like "2024 Toyota Camry" or "Honda Civic" to find vehicles in the + catalog. + + + Use Import/Export buttons to manage catalog data in bulk via CSV files. + + + )} + + {/* No Results */} + {noResults && ( + + + No Results Found + + + No vehicles match "{searchQuery}". Try a different search term. + + + )} + + {/* Results Table */} + {hasResults && ( + + {/* Bulk Actions */} + {selectedIds.size > 0 && ( + + + + )} + + + + + + + 0 && selectedIds.size < items.length} + checked={selectedIds.size === items.length && items.length > 0} + onChange={handleSelectAll} + size="small" + /> + + Year + Make + Model + Trim + Engine + Transmission + Actions + + + + {items.map((row) => ( + + + handleSelectRow(row.id)} + size="small" + /> + + {row.year} + {row.make} + {row.model} + {row.trim} + {row.engineName || '-'} + {row.transmissionType || '-'} + + + handleDeleteClick(row)} + > + + + + + + ))} + +
+
+ + +
+ )} + + {/* Audit Log */} + + + {/* Delete Confirmation Dialog */} + !deleting && setDeleteDialogOpen(false)}> + + {deleteTarget + ? 'Delete Configuration' + : `Delete ${selectedIds.size} Configuration${selectedIds.size > 1 ? 's' : ''}`} + + + {deleteTarget ? ( + + Are you sure you want to delete the configuration for{' '} + + {deleteTarget.year} {deleteTarget.make} {deleteTarget.model} {deleteTarget.trim} + + ? + + ) : ( + + Are you sure you want to delete {selectedIds.size} selected configuration + {selectedIds.size > 1 ? 's' : ''}? + + )} + + + - - ) : ( - <> - - - - Vehicle Configurations - + {deleting ? : 'Delete'} + + + - - - - + {/* Import Preview Dialog */} + !importApplyMutation.isPending && setImportDialogOpen(false)} + maxWidth="md" + fullWidth + > + Import Preview + + {importPreview && ( + + {/* Summary */} + + + To Create: {importPreview.toCreate.length} + + + To Update: {importPreview.toUpdate.length} + + + To Delete: {importPreview.toDelete.length} + + + {/* Errors */} + {importPreview.errors.length > 0 && ( + + + {importPreview.errors.length} Error(s) Found: + + + {importPreview.errors.slice(0, 10).map((err, idx) => ( +
  • + Row {err.row}: {err.error} +
  • + ))} + {importPreview.errors.length > 10 && ( +
  • ...and {importPreview.errors.length - 10} more errors
  • + )} +
    +
    + )} + + {/* Valid status */} + {importPreview.valid ? ( + + The import file is valid and ready to be applied. + + ) : ( + + Please fix the errors above before importing. + + )}
    - - - - - - - - - - - )} - - { - if (!bulkDeleting) { - setBulkDialogOpen(false); - } - }} - loading={bulkDeleting} - confirmText="Delete" - /> - - {dialogState && ( - { - if (!dialogSubmitting) { - setDialogState(null); - } - }} - onSubmit={handleDialogSubmit} - submitting={dialogSubmitting} - existingOptions={{ - makes, - models, - years, - trims, - }} - /> - )} + )} +
    + + + + +
    ); }; - -interface CatalogCombinationDialogProps { - open: boolean; - mode: 'create' | 'edit'; - row?: CatalogGridRow; - submitting: boolean; - onSubmit: (values: CatalogCombinationFormValues) => Promise; - onClose: () => void; - existingOptions: { - makes: CatalogMake[]; - models: CatalogModel[]; - years: CatalogYear[]; - trims: CatalogTrim[]; - }; -} - -const CatalogCombinationDialog: React.FC = ({ - open, - mode, - row, - submitting, - onSubmit, - onClose, - existingOptions, -}) => { - const { - control, - handleSubmit, - watch, - reset, - formState: { errors }, - } = useForm({ - defaultValues: { - makeName: row?.makeName ?? '', - modelName: row?.modelName ?? '', - year: row?.yearValue ?? new Date().getFullYear(), - trimName: row?.trimName ?? '', - engineName: row?.engineName ?? '', - transmission: row?.transmission ?? 'Automatic', - }, - }); - - useEffect(() => { - reset({ - makeName: row?.makeName ?? '', - modelName: row?.modelName ?? '', - year: row?.yearValue ?? new Date().getFullYear(), - trimName: row?.trimName ?? '', - engineName: row?.engineName ?? '', - transmission: row?.transmission ?? 'Automatic', - }); - }, [reset, row]); - - const selectedMakeName = watch('makeName'); - const selectedModelName = watch('modelName'); - const selectedYearValue = watch('year'); - - const filteredModels = useMemo(() => { - if (!selectedMakeName) { - return existingOptions.models; - } - const make = existingOptions.makes.find( - (item) => normalizeName(item.name) === normalizeName(selectedMakeName) - ); - if (!make) { - return []; - } - return existingOptions.models.filter((model) => model.makeId === make.id); - }, [existingOptions.models, existingOptions.makes, selectedMakeName]); - - const filteredTrims = useMemo(() => { - if (!selectedYearValue) { - return existingOptions.trims; - } - - const matchingYears = existingOptions.years.filter( - (item) => item.year === selectedYearValue - ); - - if (matchingYears.length === 0) { - return []; - } - - if (selectedModelName) { - const model = existingOptions.models.find( - (item) => normalizeName(item.name) === normalizeName(selectedModelName) - ); - if (model) { - const yearForModel = matchingYears.find((year) => year.modelId === model.id); - if (yearForModel) { - return existingOptions.trims.filter((trim) => trim.yearId === yearForModel.id); - } - } - } - - if (matchingYears.length === 1) { - return existingOptions.trims.filter((trim) => trim.yearId === matchingYears[0].id); - } - - return existingOptions.trims; - }, [existingOptions.trims, existingOptions.years, existingOptions.models, selectedYearValue, selectedModelName]); - - const submitHandler = handleSubmit((values) => onSubmit(values)); - - return ( - - - {mode === 'create' ? 'Add Vehicle Configuration' : 'Edit Vehicle Configuration'} - - - - ( - make.name)} - onChange={(_, value) => field.onChange(value ?? '')} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - model.name)} - onChange={(_, value) => field.onChange(value ?? '')} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - { - const value = event.target.value; - field.onChange(value === '' ? undefined : Number(value)); - }} - error={Boolean(errors.year)} - helperText={errors.year?.message} - /> - )} - /> - - ( - trim.name)} - onChange={(_, value) => field.onChange(value ?? '')} - renderInput={(params) => ( - - )} - /> - )} - /> - - ( - - )} - /> - - ( - - {transmissionOptions.map((option) => ( - - {option} - - ))} - - )} - /> - - - - - - - - ); -};