Homepage Redesign
This commit is contained in:
88
archive/platform-services/README.md
Normal file
88
archive/platform-services/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Archived Platform Services
|
||||
|
||||
## vehicles/
|
||||
**Archived**: 2025-11-03
|
||||
**Reason**: Integrated into backend as platform feature module
|
||||
|
||||
This Python FastAPI service was replaced by the TypeScript platform feature in `backend/src/features/platform/`.
|
||||
|
||||
### What Was This Service?
|
||||
The vehicles platform service provided:
|
||||
- Vehicle hierarchical data (makes/models/trims/engines) via PostgreSQL
|
||||
- VIN decoding via NHTSA vPIC API with circuit breaker
|
||||
- Redis caching (6-hour TTL for vehicle data, 7-day TTL for VIN decode)
|
||||
- JWT authentication
|
||||
- Health checks and monitoring
|
||||
|
||||
### Why Was It Archived?
|
||||
1. **Architecture Simplification**: Reduced from 6 to 5 containers
|
||||
2. **Technology Stack Unification**: Consolidated on Node.js/TypeScript
|
||||
3. **Development Experience**: Eliminated inter-service HTTP calls
|
||||
4. **Deployment Complexity**: Simplified Docker compose configuration
|
||||
|
||||
### Migration Details
|
||||
See: `docs/PLATFORM-INTEGRATION-MIGRATION.md`
|
||||
|
||||
### New Implementation
|
||||
Location: `backend/src/features/platform/`
|
||||
API Endpoints: `/api/platform/*`
|
||||
Language: TypeScript (Fastify)
|
||||
Database: Same PostgreSQL vehicles schema
|
||||
Caching: Same Redis strategy
|
||||
|
||||
### Original Architecture
|
||||
```
|
||||
mvp-platform (Python FastAPI)
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── api/
|
||||
│ ├── routes/
|
||||
│ ├── services/
|
||||
│ ├── repositories/
|
||||
│ └── models/
|
||||
└── tests/
|
||||
```
|
||||
|
||||
### New Architecture
|
||||
```
|
||||
backend/src/features/platform/ (TypeScript Fastify)
|
||||
├── api/
|
||||
│ ├── platform.routes.ts
|
||||
│ └── platform.controller.ts
|
||||
├── domain/
|
||||
│ ├── vin-decode.service.ts
|
||||
│ ├── vehicle-data.service.ts
|
||||
│ └── platform-cache.service.ts
|
||||
├── data/
|
||||
│ ├── vehicle-data.repository.ts
|
||||
│ └── vpic-client.ts
|
||||
└── tests/
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
### Restoration (if needed)
|
||||
If you need to restore this service:
|
||||
```bash
|
||||
# 1. Move back from archive
|
||||
mv archive/platform-services/vehicles mvp-platform-services/
|
||||
|
||||
# 2. Restore docker-compose.yml configuration
|
||||
git restore docker-compose.yml
|
||||
|
||||
# 3. Rebuild containers
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
|
||||
# 4. Verify 6 containers running
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
### Permanent Deletion
|
||||
This directory can be permanently deleted after:
|
||||
1. Platform feature proven stable in production (30+ days)
|
||||
2. All stakeholders approve removal
|
||||
3. Backup created of this archive directory
|
||||
4. Git history confirms safe recovery if needed
|
||||
|
||||
Do not delete before 2025-12-03 at earliest.
|
||||
44
archive/platform-services/vehicles/README.md
Normal file
44
archive/platform-services/vehicles/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# MVP Platform Vehicles Service
|
||||
|
||||
For full platform architecture and integration patterns, see `docs/PLATFORM-SERVICES.md`.
|
||||
|
||||
## Schema Bootstrapping (Docker-First)
|
||||
- Database: PostgreSQL, service `mvp-platform-vehicles-db`.
|
||||
- On first start, schema files from `mvp-platform-services/vehicles/sql/schema` are executed automatically because the folder is mounted to `/docker-entrypoint-initdb.d` in `docker-compose.yml`.
|
||||
- Files run in lexicographic order:
|
||||
- `001_schema.sql` – creates `vehicles` schema and tables
|
||||
- `002_constraints_indexes.sql` – adds uniques and indexes
|
||||
- `003_seed_minimal.sql` – seeds minimal dropdown data for sanity checks
|
||||
|
||||
## When Do Files Run?
|
||||
- Only on the initial database initialization (i.e., when the Postgres data volume is empty).
|
||||
- Subsequent `make start` runs will not reapply these files unless you reset the volume.
|
||||
|
||||
## Applying Schema Changes
|
||||
- Option 1 (fresh reset):
|
||||
1. `make clean` to remove volumes
|
||||
2. `make start` (the `.sql` files will be reapplied)
|
||||
- Option 2 (manual apply to existing DB):
|
||||
- Exec into the DB container and run the SQL files in order:
|
||||
```bash
|
||||
docker compose exec mvp-platform-vehicles-db bash -lc "psql -U mvp_platform_user -d vehicles -f /docker-entrypoint-initdb.d/001_schema.sql"
|
||||
docker compose exec mvp-platform-vehicles-db bash -lc "psql -U mvp_platform_user -d vehicles -f /docker-entrypoint-initdb.d/002_constraints_indexes.sql"
|
||||
docker compose exec mvp-platform-vehicles-db bash -lc "psql -U mvp_platform_user -d vehicles -f /docker-entrypoint-initdb.d/003_seed_minimal.sql"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
```bash
|
||||
make start
|
||||
make logs-platform-vehicles # View API + DB logs
|
||||
```
|
||||
|
||||
## Endpoint Summary (Auth Required: Authorization: Bearer <API_KEY>)
|
||||
- `GET /api/v1/vehicles/years` → `[number]`
|
||||
- `GET /api/v1/vehicles/makes?year=YYYY` → `{ makes: [{id,name}] }`
|
||||
- `GET /api/v1/vehicles/models?year=YYYY&make_id=ID` → `{ models: [...] }`
|
||||
- `GET /api/v1/vehicles/trims?year=YYYY&make_id=ID&model_id=ID` → `{ trims: [...] }`
|
||||
- `GET /api/v1/vehicles/engines?year=YYYY&make_id=ID&model_id=ID&trim_id=ID` → `{ engines: [...] }`
|
||||
|
||||
## Notes
|
||||
- Transmissions and performance tables exist for future use; no endpoints yet.
|
||||
- VIN decode endpoints are pending rebuild and not documented here.
|
||||
0
archive/platform-services/vehicles/api/__init__.py
Normal file
0
archive/platform-services/vehicles/api/__init__.py
Normal file
50
archive/platform-services/vehicles/api/config.py
Normal file
50
archive/platform-services/vehicles/api/config.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import List
|
||||
|
||||
# Docker-first: load secrets from mounted files when env vars are absent
|
||||
_PG_SECRET_FILE = os.getenv("POSTGRES_PASSWORD_FILE", "/run/secrets/postgres-password")
|
||||
if not os.getenv("POSTGRES_PASSWORD"):
|
||||
try:
|
||||
with open(_PG_SECRET_FILE, 'r') as f:
|
||||
os.environ["POSTGRES_PASSWORD"] = f.read().strip()
|
||||
except Exception:
|
||||
# Leave as-is; connection will fail loudly if missing
|
||||
pass
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application configuration"""
|
||||
|
||||
# Database settings (shared mvp-postgres)
|
||||
POSTGRES_HOST: str = os.getenv("POSTGRES_HOST", "mvp-postgres")
|
||||
POSTGRES_PORT: int = int(os.getenv("POSTGRES_PORT", "5432"))
|
||||
POSTGRES_USER: str = os.getenv("POSTGRES_USER", "postgres")
|
||||
POSTGRES_PASSWORD: str = os.getenv("POSTGRES_PASSWORD", "")
|
||||
POSTGRES_DATABASE: str = os.getenv("POSTGRES_DATABASE", "motovaultpro")
|
||||
|
||||
# Redis settings (shared mvp-redis)
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "mvp-redis")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "1")) # Use DB 1 to separate from backend
|
||||
|
||||
# Database connection pool settings
|
||||
DATABASE_MIN_CONNECTIONS: int = int(os.getenv("DATABASE_MIN_CONNECTIONS", "5"))
|
||||
DATABASE_MAX_CONNECTIONS: int = int(os.getenv("DATABASE_MAX_CONNECTIONS", "20"))
|
||||
|
||||
# Cache settings
|
||||
CACHE_TTL: int = int(os.getenv("CACHE_TTL", "3600")) # 1 hour default
|
||||
|
||||
# Application settings
|
||||
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
|
||||
CORS_ORIGINS: List[str] = [
|
||||
"http://localhost:3000",
|
||||
"https://motovaultpro.com",
|
||||
"http://localhost:3001"
|
||||
]
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Get application settings"""
|
||||
return Settings()
|
||||
40
archive/platform-services/vehicles/api/dependencies.py
Normal file
40
archive/platform-services/vehicles/api/dependencies.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import asyncpg
|
||||
import redis.asyncio as redis
|
||||
from fastapi import Request, Depends, HTTPException
|
||||
import logging
|
||||
from .config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
async def get_db_pool(request: Request) -> asyncpg.Pool:
|
||||
"""Get database pool from app state"""
|
||||
return request.app.state.db_pool
|
||||
|
||||
async def get_db(request: Request) -> asyncpg.Connection:
|
||||
"""Get database connection"""
|
||||
pool = await get_db_pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
yield conn
|
||||
|
||||
async def get_redis_client(request: Request) -> redis.Redis:
|
||||
"""Get Redis client from app state"""
|
||||
return request.app.state.redis_client
|
||||
|
||||
async def get_cache(request: Request):
|
||||
"""Get cache service from app state"""
|
||||
return request.app.state.cache_service
|
||||
|
||||
async def verify_bearer_token(request: Request) -> str:
|
||||
"""Verify Bearer token for service-to-service authentication
|
||||
|
||||
Expects header: Authorization: Bearer <token>
|
||||
Compares token to settings.API_KEY
|
||||
"""
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
|
||||
token = auth_header.split(" ", 1)[1].strip()
|
||||
if token != settings.API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid service token")
|
||||
return token
|
||||
202
archive/platform-services/vehicles/api/main.py
Normal file
202
archive/platform-services/vehicles/api/main.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import asyncpg
|
||||
import redis.asyncio as redis
|
||||
import time
|
||||
|
||||
from .config import get_settings
|
||||
from .dependencies import get_db_pool, get_redis_client, get_cache, verify_bearer_token
|
||||
from .routes import vehicles, vin
|
||||
from .models.responses import HealthResponse
|
||||
from .services.cache_service import CacheService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
# Startup
|
||||
logger.info("Starting MVP Platform Vehicles API...")
|
||||
|
||||
# Initialize database pool
|
||||
try:
|
||||
app.state.db_pool = await asyncpg.create_pool(
|
||||
host=settings.POSTGRES_HOST,
|
||||
port=settings.POSTGRES_PORT,
|
||||
user=settings.POSTGRES_USER,
|
||||
password=settings.POSTGRES_PASSWORD,
|
||||
database=settings.POSTGRES_DATABASE,
|
||||
min_size=settings.DATABASE_MIN_CONNECTIONS,
|
||||
max_size=settings.DATABASE_MAX_CONNECTIONS,
|
||||
command_timeout=30
|
||||
)
|
||||
logger.info("Database pool initialized")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database pool: {e}")
|
||||
raise
|
||||
|
||||
# Initialize Redis client
|
||||
try:
|
||||
app.state.redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DB,
|
||||
decode_responses=False,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=5
|
||||
)
|
||||
# Test connection
|
||||
await app.state.redis_client.ping()
|
||||
logger.info("Redis client initialized")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize Redis client: {e}")
|
||||
app.state.redis_client = None
|
||||
|
||||
# Initialize cache service
|
||||
app.state.cache_service = CacheService(
|
||||
app.state.redis_client,
|
||||
enabled=bool(app.state.redis_client),
|
||||
default_ttl=settings.CACHE_TTL
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("Shutting down MVP Platform Vehicles API...")
|
||||
|
||||
if hasattr(app.state, 'db_pool') and app.state.db_pool:
|
||||
await app.state.db_pool.close()
|
||||
logger.info("Database pool closed")
|
||||
|
||||
if hasattr(app.state, 'redis_client') and app.state.redis_client:
|
||||
await app.state.redis_client.aclose()
|
||||
logger.info("Redis client closed")
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="MVP Platform Vehicles API",
|
||||
description="Hierarchical Vehicle API with VIN decoding for MotoVaultPro platform services",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs" if settings.DEBUG else None,
|
||||
redoc_url="/redoc" if settings.DEBUG else None
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Request timing middleware
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
|
||||
# Global exception handler
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logger.error(f"Unhandled exception in {request.method} {request.url.path}: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error"}
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(vehicles.router, prefix="/api/v1", dependencies=[Depends(verify_bearer_token)])
|
||||
app.include_router(vin.router, prefix="/api/v1", dependencies=[Depends(verify_bearer_token)])
|
||||
|
||||
# Health check endpoint
|
||||
@app.api_route("/health", methods=["GET", "HEAD"], response_model=HealthResponse)
|
||||
async def health_check(request: Request):
|
||||
"""Health check endpoint"""
|
||||
db_status = "ok"
|
||||
cache_status = "ok"
|
||||
|
||||
# Check database
|
||||
try:
|
||||
db_pool = request.app.state.db_pool
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.fetchval("SELECT 1")
|
||||
except Exception as e:
|
||||
logger.error(f"Database health check failed: {e}")
|
||||
db_status = "error"
|
||||
|
||||
# Check cache
|
||||
try:
|
||||
cache = request.app.state.cache_service
|
||||
if cache and cache.enabled:
|
||||
await cache.redis.ping()
|
||||
else:
|
||||
cache_status = "disabled"
|
||||
except Exception as e:
|
||||
logger.error(f"Cache health check failed: {e}")
|
||||
cache_status = "error"
|
||||
|
||||
overall_status = "ok" if db_status == "ok" else "degraded"
|
||||
|
||||
return HealthResponse(
|
||||
status=overall_status,
|
||||
database=db_status,
|
||||
cache=cache_status,
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint with API information"""
|
||||
return {
|
||||
"name": "MVP Platform Vehicles API",
|
||||
"version": "1.0.0",
|
||||
"description": "Hierarchical Vehicle API with VIN decoding",
|
||||
"docs_url": "/docs" if settings.DEBUG else "Contact administrator for documentation",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"makes": "/api/v1/vehicles/makes?year=2024",
|
||||
"models": "/api/v1/vehicles/models?year=2024&make_id=1",
|
||||
"trims": "/api/v1/vehicles/trims?year=2024&make_id=1&model_id=1",
|
||||
"engines": "/api/v1/vehicles/engines?year=2024&make_id=1&model_id=1",
|
||||
"transmissions": "/api/v1/vehicles/transmissions?year=2024&make_id=1&model_id=1",
|
||||
"vin_decode": "/api/v1/vehicles/vindecode"
|
||||
}
|
||||
}
|
||||
|
||||
# Cache stats endpoint
|
||||
@app.get("/api/v1/cache/stats")
|
||||
async def cache_stats(request: Request, token: str = Depends(verify_bearer_token)):
|
||||
"""Get cache statistics"""
|
||||
try:
|
||||
cache = request.app.state.cache_service
|
||||
stats = await cache.get_stats()
|
||||
return stats
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get cache stats: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve cache statistics")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"api.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG,
|
||||
log_level="info"
|
||||
)
|
||||
84
archive/platform-services/vehicles/api/models/responses.py
Normal file
84
archive/platform-services/vehicles/api/models/responses.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
class MakeItem(BaseModel):
|
||||
"""Make item response model"""
|
||||
id: int
|
||||
name: str
|
||||
|
||||
class ModelItem(BaseModel):
|
||||
"""Model item response model"""
|
||||
id: int
|
||||
name: str
|
||||
|
||||
class TrimItem(BaseModel):
|
||||
"""Trim item response model"""
|
||||
id: int
|
||||
name: str
|
||||
|
||||
class EngineItem(BaseModel):
|
||||
"""Engine item response model"""
|
||||
id: int
|
||||
name: str
|
||||
|
||||
class TransmissionItem(BaseModel):
|
||||
"""Transmission item response model"""
|
||||
name: str
|
||||
|
||||
class MakesResponse(BaseModel):
|
||||
"""Makes response model"""
|
||||
makes: List[MakeItem]
|
||||
|
||||
class YearsResponse(BaseModel):
|
||||
"""Years response model"""
|
||||
years: List[int]
|
||||
|
||||
class ModelsResponse(BaseModel):
|
||||
"""Models response model"""
|
||||
models: List[ModelItem]
|
||||
|
||||
class TrimsResponse(BaseModel):
|
||||
"""Trims response model"""
|
||||
trims: List[TrimItem]
|
||||
|
||||
class EnginesResponse(BaseModel):
|
||||
"""Engines response model"""
|
||||
engines: List[EngineItem]
|
||||
|
||||
class TransmissionsResponse(BaseModel):
|
||||
"""Transmissions response model"""
|
||||
transmissions: List[TransmissionItem]
|
||||
|
||||
class VINDecodeResult(BaseModel):
|
||||
"""VIN decode result model"""
|
||||
make: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
year: Optional[int] = None
|
||||
trim_name: Optional[str] = None
|
||||
engine_description: Optional[str] = None
|
||||
transmission_description: Optional[str] = None
|
||||
horsepower: Optional[float] = None
|
||||
torque: Optional[float] = None
|
||||
top_speed: Optional[float] = None
|
||||
fuel: Optional[str] = None
|
||||
confidence_score: Optional[float] = None
|
||||
vehicle_type: Optional[str] = None
|
||||
|
||||
class VINDecodeRequest(BaseModel):
|
||||
"""VIN decode request model"""
|
||||
vin: str
|
||||
|
||||
class VINDecodeResponse(BaseModel):
|
||||
"""VIN decode response model"""
|
||||
vin: str
|
||||
result: Optional[VINDecodeResult]
|
||||
success: bool
|
||||
error: Optional[str] = None
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response model"""
|
||||
status: str
|
||||
database: str
|
||||
cache: str
|
||||
version: str
|
||||
etl_last_run: Optional[str] = None
|
||||
@@ -0,0 +1,79 @@
|
||||
import asyncpg
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class VehiclesRepository:
|
||||
"""Repository for hierarchical vehicle queries against normalized schema"""
|
||||
|
||||
async def get_years(self, db: asyncpg.Connection) -> List[int]:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
SELECT DISTINCT year
|
||||
FROM vehicles.model_year
|
||||
ORDER BY year DESC
|
||||
"""
|
||||
)
|
||||
return [r["year"] for r in rows]
|
||||
|
||||
async def get_makes(self, db: asyncpg.Connection, year: int) -> List[Dict]:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
SELECT DISTINCT ma.id, ma.name
|
||||
FROM vehicles.make ma
|
||||
JOIN vehicles.model mo ON mo.make_id = ma.id
|
||||
JOIN vehicles.model_year my ON my.model_id = mo.id AND my.year = $1
|
||||
ORDER BY ma.name
|
||||
""",
|
||||
year,
|
||||
)
|
||||
return [{"id": r["id"], "name": r["name"]} for r in rows]
|
||||
|
||||
async def get_models(self, db: asyncpg.Connection, year: int, make_id: int) -> List[Dict]:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
SELECT DISTINCT mo.id, mo.name
|
||||
FROM vehicles.model mo
|
||||
JOIN vehicles.model_year my ON my.model_id = mo.id AND my.year = $1
|
||||
WHERE mo.make_id = $2
|
||||
ORDER BY mo.name
|
||||
""",
|
||||
year,
|
||||
make_id,
|
||||
)
|
||||
return [{"id": r["id"], "name": r["name"]} for r in rows]
|
||||
|
||||
async def get_trims(self, db: asyncpg.Connection, year: int, model_id: int) -> List[Dict]:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
SELECT t.id, t.name
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id
|
||||
WHERE my.year = $1 AND my.model_id = $2
|
||||
ORDER BY t.name
|
||||
""",
|
||||
year,
|
||||
model_id,
|
||||
)
|
||||
return [{"id": r["id"], "name": r["name"]} for r in rows]
|
||||
|
||||
async def get_engines(
|
||||
self, db: asyncpg.Connection, year: int, model_id: int, trim_id: int
|
||||
) -> List[Dict]:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
SELECT DISTINCT e.id, e.name
|
||||
FROM vehicles.engine e
|
||||
JOIN vehicles.trim_engine te ON te.engine_id = e.id
|
||||
JOIN vehicles.trim t ON t.id = te.trim_id
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id
|
||||
WHERE my.year = $1
|
||||
AND my.model_id = $2
|
||||
AND t.id = $3
|
||||
ORDER BY e.name
|
||||
""",
|
||||
year,
|
||||
model_id,
|
||||
trim_id,
|
||||
)
|
||||
return [{"id": r["id"], "name": r["name"]} for r in rows]
|
||||
|
||||
116
archive/platform-services/vehicles/api/routes/vehicles.py
Normal file
116
archive/platform-services/vehicles/api/routes/vehicles.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
import asyncpg
|
||||
from ..dependencies import get_db, get_cache
|
||||
# DropdownService deprecated; using normalized schema service
|
||||
from ..services.vehicles_service import VehiclesService
|
||||
from ..repositories.vehicles_repository import VehiclesRepository
|
||||
from ..services.cache_service import CacheService
|
||||
from ..models.responses import (
|
||||
MakesResponse, ModelsResponse, TrimsResponse,
|
||||
EnginesResponse,
|
||||
MakeItem, ModelItem, TrimItem, EngineItem
|
||||
)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/vehicles", tags=["Vehicles"])
|
||||
|
||||
@router.get("/years", response_model=list[int])
|
||||
async def get_years(
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache),
|
||||
):
|
||||
"""Get available model years (distinct, desc)"""
|
||||
service = VehiclesService(cache, VehiclesRepository())
|
||||
return await service.get_years(db)
|
||||
|
||||
@router.get("/makes", response_model=MakesResponse)
|
||||
async def get_makes(
|
||||
year: int = Query(..., description="Model year", ge=1980, le=2050),
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache)
|
||||
):
|
||||
"""Get makes for a specific year
|
||||
|
||||
Hierarchical API: First level - requires year parameter only
|
||||
"""
|
||||
try:
|
||||
service = VehiclesService(cache, VehiclesRepository())
|
||||
makes = await service.get_makes(db, year)
|
||||
return MakesResponse(makes=[MakeItem(**m) for m in makes])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get makes for year {year}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve makes for year {year}"
|
||||
)
|
||||
|
||||
@router.get("/models", response_model=ModelsResponse)
|
||||
async def get_models(
|
||||
year: int = Query(..., description="Model year", ge=1980, le=2050),
|
||||
make_id: int = Query(..., description="Make ID", ge=1),
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache)
|
||||
):
|
||||
"""Get models for year and make
|
||||
|
||||
Hierarchical API: Second level - requires year and make_id parameters
|
||||
"""
|
||||
try:
|
||||
service = VehiclesService(cache, VehiclesRepository())
|
||||
models = await service.get_models(db, year, make_id)
|
||||
return ModelsResponse(models=[ModelItem(**m) for m in models])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get models for year {year}, make {make_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve models for year {year}, make {make_id}"
|
||||
)
|
||||
|
||||
@router.get("/trims", response_model=TrimsResponse)
|
||||
async def get_trims(
|
||||
year: int = Query(..., description="Model year", ge=1980, le=2050),
|
||||
make_id: int = Query(..., description="Make ID", ge=1),
|
||||
model_id: int = Query(..., description="Model ID", ge=1),
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache)
|
||||
):
|
||||
"""Get trims for year, make, and model
|
||||
|
||||
Hierarchical API: Third level - requires year, make_id, and model_id parameters
|
||||
"""
|
||||
try:
|
||||
service = VehiclesService(cache, VehiclesRepository())
|
||||
trims = await service.get_trims(db, year, model_id)
|
||||
return TrimsResponse(trims=[TrimItem(**t) for t in trims])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get trims for year {year}, make {make_id}, model {model_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve trims for year {year}, make {make_id}, model {model_id}"
|
||||
)
|
||||
|
||||
@router.get("/engines", response_model=EnginesResponse)
|
||||
async def get_engines(
|
||||
year: int = Query(..., description="Model year", ge=1980, le=2050),
|
||||
make_id: int = Query(..., description="Make ID", ge=1),
|
||||
model_id: int = Query(..., description="Model ID", ge=1),
|
||||
trim_id: int = Query(..., description="Trim ID", ge=1),
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache)
|
||||
):
|
||||
"""Get engines for year, make, model, and trim"""
|
||||
try:
|
||||
service = VehiclesService(cache, VehiclesRepository())
|
||||
engines = await service.get_engines(db, year, model_id, trim_id)
|
||||
return EnginesResponse(engines=[EngineItem(**e) for e in engines])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to get engines for year {year}, make {make_id}, model {model_id}, trim {trim_id}: {e}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=(
|
||||
f"Failed to retrieve engines for year {year}, make {make_id}, model {model_id}, trim {trim_id}"
|
||||
)
|
||||
)
|
||||
110
archive/platform-services/vehicles/api/routes/vin.py
Normal file
110
archive/platform-services/vehicles/api/routes/vin.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
import asyncpg
|
||||
from ..dependencies import get_db, get_cache
|
||||
from ..services.cache_service import CacheService
|
||||
from ..models.responses import VINDecodeRequest, VINDecodeResponse, VINDecodeResult
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/vehicles", tags=["VIN Decoding"])
|
||||
|
||||
def validate_vin(vin: str) -> bool:
|
||||
"""Validate VIN format"""
|
||||
if len(vin) != 17:
|
||||
return False
|
||||
|
||||
# VIN cannot contain I, O, Q
|
||||
if any(char in vin.upper() for char in ['I', 'O', 'Q']):
|
||||
return False
|
||||
|
||||
# Must be alphanumeric
|
||||
if not re.match(r'^[A-HJ-NPR-Z0-9]{17}$', vin.upper()):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@router.post("/vindecode", response_model=VINDecodeResponse)
|
||||
async def decode_vin(
|
||||
request: VINDecodeRequest,
|
||||
db: asyncpg.Connection = Depends(get_db),
|
||||
cache: CacheService = Depends(get_cache)
|
||||
):
|
||||
"""Decode VIN using PostgreSQL function with MSSQL parity
|
||||
|
||||
Uses the vehicles.f_decode_vin() function to decode VIN with confidence scoring
|
||||
"""
|
||||
vin = request.vin.upper().strip()
|
||||
|
||||
# Validate VIN format
|
||||
if not validate_vin(vin):
|
||||
return VINDecodeResponse(
|
||||
vin=vin,
|
||||
result=None,
|
||||
success=False,
|
||||
error="Invalid VIN format"
|
||||
)
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"vin:decode:{vin}"
|
||||
cached_result = await cache.get(cache_key)
|
||||
if cached_result:
|
||||
logger.debug(f"VIN decode result for {vin} retrieved from cache")
|
||||
return VINDecodeResponse(**cached_result)
|
||||
|
||||
try:
|
||||
# Call PostgreSQL VIN decode function
|
||||
query = """
|
||||
SELECT * FROM vehicles.f_decode_vin($1)
|
||||
"""
|
||||
|
||||
row = await db.fetchrow(query, vin)
|
||||
|
||||
if row:
|
||||
result = VINDecodeResult(
|
||||
make=row['make'],
|
||||
model=row['model'],
|
||||
year=row['year'],
|
||||
trim_name=row['trim_name'],
|
||||
engine_description=row['engine_description'],
|
||||
transmission_description=row['transmission_description'],
|
||||
horsepower=row.get('horsepower'),
|
||||
torque=row.get('torque'),
|
||||
top_speed=row.get('top_speed'),
|
||||
fuel=row.get('fuel'),
|
||||
confidence_score=float(row['confidence_score']) if row['confidence_score'] else 0.0,
|
||||
vehicle_type=row.get('vehicle_type')
|
||||
)
|
||||
|
||||
response = VINDecodeResponse(
|
||||
vin=vin,
|
||||
result=result,
|
||||
success=True
|
||||
)
|
||||
|
||||
# Cache successful decode for 30 days
|
||||
await cache.set(cache_key, response.dict(), ttl=30*24*3600)
|
||||
|
||||
logger.info(f"Successfully decoded VIN {vin}: {result.make} {result.model} {result.year}")
|
||||
return response
|
||||
else:
|
||||
# No result found
|
||||
response = VINDecodeResponse(
|
||||
vin=vin,
|
||||
result=None,
|
||||
success=False,
|
||||
error="VIN not found in database"
|
||||
)
|
||||
|
||||
# Cache negative result for 1 hour
|
||||
await cache.set(cache_key, response.dict(), ttl=3600)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decode VIN {vin}: {e}")
|
||||
return VINDecodeResponse(
|
||||
vin=vin,
|
||||
result=None,
|
||||
success=False,
|
||||
error="Internal server error during VIN decoding"
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
import redis.asyncio as redis
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CacheService:
|
||||
"""Redis cache service with JSON serialization"""
|
||||
|
||||
def __init__(self, redis_client: Optional[redis.Redis], enabled: bool = True, default_ttl: int = 3600):
|
||||
self.redis = redis_client
|
||||
self.enabled = enabled and redis_client is not None
|
||||
self.default_ttl = default_ttl
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
"""Get value from cache"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
try:
|
||||
value = await self.redis.get(key)
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Cache get error for key {key}: {e}")
|
||||
return None
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
|
||||
"""Set value in cache"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
try:
|
||||
ttl = ttl or self.default_ttl
|
||||
json_value = json.dumps(value, default=str) # Handle datetime objects
|
||||
await self.redis.setex(key, ttl, json_value)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Cache set error for key {key}: {e}")
|
||||
return False
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete key from cache"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
try:
|
||||
deleted = await self.redis.delete(key)
|
||||
return deleted > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Cache delete error for key {key}: {e}")
|
||||
return False
|
||||
|
||||
async def invalidate_dropdown_cache(self) -> int:
|
||||
"""Invalidate all dropdown cache entries"""
|
||||
if not self.enabled:
|
||||
return 0
|
||||
|
||||
try:
|
||||
pattern = "dropdown:*"
|
||||
keys = await self.redis.keys(pattern)
|
||||
if keys:
|
||||
deleted = await self.redis.delete(*keys)
|
||||
logger.info(f"Invalidated {deleted} dropdown cache entries")
|
||||
return deleted
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Cache invalidation error: {e}")
|
||||
return 0
|
||||
|
||||
async def get_stats(self) -> dict:
|
||||
"""Get cache statistics"""
|
||||
if not self.enabled:
|
||||
return {"enabled": False}
|
||||
|
||||
try:
|
||||
info = await self.redis.info("memory")
|
||||
return {
|
||||
"enabled": True,
|
||||
"used_memory": info.get("used_memory_human"),
|
||||
"used_memory_peak": info.get("used_memory_peak_human"),
|
||||
"connected_clients": await self.redis.client_list()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Cache stats error: {e}")
|
||||
return {"enabled": True, "error": str(e)}
|
||||
@@ -0,0 +1,58 @@
|
||||
import asyncpg
|
||||
from typing import List, Dict
|
||||
from ..services.cache_service import CacheService
|
||||
from ..repositories.vehicles_repository import VehiclesRepository
|
||||
|
||||
|
||||
class VehiclesService:
|
||||
def __init__(self, cache: CacheService, repo: VehiclesRepository | None = None):
|
||||
self.cache = cache
|
||||
self.repo = repo or VehiclesRepository()
|
||||
|
||||
async def get_years(self, db: asyncpg.Connection) -> List[int]:
|
||||
cache_key = "dropdown:years"
|
||||
cached = await self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
years = await self.repo.get_years(db)
|
||||
await self.cache.set(cache_key, years, ttl=6 * 3600)
|
||||
return years
|
||||
|
||||
async def get_makes(self, db: asyncpg.Connection, year: int) -> List[Dict]:
|
||||
cache_key = f"dropdown:makes:{year}"
|
||||
cached = await self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
makes = await self.repo.get_makes(db, year)
|
||||
await self.cache.set(cache_key, makes, ttl=6 * 3600)
|
||||
return makes
|
||||
|
||||
async def get_models(self, db: asyncpg.Connection, year: int, make_id: int) -> List[Dict]:
|
||||
cache_key = f"dropdown:models:{year}:{make_id}"
|
||||
cached = await self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
models = await self.repo.get_models(db, year, make_id)
|
||||
await self.cache.set(cache_key, models, ttl=6 * 3600)
|
||||
return models
|
||||
|
||||
async def get_trims(self, db: asyncpg.Connection, year: int, model_id: int) -> List[Dict]:
|
||||
cache_key = f"dropdown:trims:{year}:{model_id}"
|
||||
cached = await self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
trims = await self.repo.get_trims(db, year, model_id)
|
||||
await self.cache.set(cache_key, trims, ttl=6 * 3600)
|
||||
return trims
|
||||
|
||||
async def get_engines(
|
||||
self, db: asyncpg.Connection, year: int, model_id: int, trim_id: int
|
||||
) -> List[Dict]:
|
||||
cache_key = f"dropdown:engines:{year}:{model_id}:{trim_id}"
|
||||
cached = await self.cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
engines = await self.repo.get_engines(db, year, model_id, trim_id)
|
||||
await self.cache.set(cache_key, engines, ttl=6 * 3600)
|
||||
return engines
|
||||
|
||||
30
archive/platform-services/vehicles/docker/Dockerfile.api
Normal file
30
archive/platform-services/vehicles/docker/Dockerfile.api
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements-api.txt .
|
||||
RUN pip install --no-cache-dir -r requirements-api.txt
|
||||
|
||||
# Copy application code
|
||||
COPY api/ ./api/
|
||||
|
||||
# Set Python path
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:8000/health || exit 1
|
||||
|
||||
# Run application
|
||||
CMD ["python", "-m", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
69
archive/platform-services/vehicles/makes.json
Normal file
69
archive/platform-services/vehicles/makes.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"manufacturers": [
|
||||
"Acura",
|
||||
"Alfa Romeo",
|
||||
"Aston Martin",
|
||||
"Audi",
|
||||
"BMW",
|
||||
"Bentley",
|
||||
"Buick",
|
||||
"Cadillac",
|
||||
"Chevrolet",
|
||||
"Chrysler",
|
||||
"Daewoo",
|
||||
"Dodge",
|
||||
"Eagle",
|
||||
"Ferrari",
|
||||
"Fiat",
|
||||
"Fisker",
|
||||
"Ford",
|
||||
"GMC",
|
||||
"Genesis",
|
||||
"Geo",
|
||||
"Honda",
|
||||
"Hummer",
|
||||
"Hyundai",
|
||||
"Infiniti",
|
||||
"Isuzu",
|
||||
"Jaguar",
|
||||
"Jeep",
|
||||
"Kia",
|
||||
"Lamborghini",
|
||||
"Land Rover",
|
||||
"Lexus",
|
||||
"Lincoln",
|
||||
"Lotus",
|
||||
"Mazda",
|
||||
"Maserati",
|
||||
"Maybach",
|
||||
"McLaren",
|
||||
"Mercedes-Benz",
|
||||
"Mercury",
|
||||
"Mini",
|
||||
"Mitsubishi",
|
||||
"Nissan",
|
||||
"Oldsmobile",
|
||||
"Panoz",
|
||||
"Plymouth",
|
||||
"Polestar",
|
||||
"Pontiac",
|
||||
"Porsche",
|
||||
"Ram",
|
||||
"Rivian",
|
||||
"Rolls Royce",
|
||||
"Saab",
|
||||
"Saturn",
|
||||
"Scion",
|
||||
"Smart",
|
||||
"Subaru",
|
||||
"Suzuki",
|
||||
"Tesla",
|
||||
"Toyota",
|
||||
"Volkswagen",
|
||||
"Volvo",
|
||||
"Karma",
|
||||
"Pagani",
|
||||
"Koenigsegg",
|
||||
"Lucid"
|
||||
]
|
||||
}
|
||||
6
archive/platform-services/vehicles/requirements-api.txt
Normal file
6
archive/platform-services/vehicles/requirements-api.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
asyncpg==0.29.0
|
||||
redis==5.0.1
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
73
archive/platform-services/vehicles/sql/schema/001_schema.sql
Normal file
73
archive/platform-services/vehicles/sql/schema/001_schema.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- Vehicles Platform Service Schema (baseline)
|
||||
CREATE SCHEMA IF NOT EXISTS vehicles;
|
||||
|
||||
-- Makes
|
||||
CREATE TABLE IF NOT EXISTS vehicles.make (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Models
|
||||
CREATE TABLE IF NOT EXISTS vehicles.model (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
make_id BIGINT NOT NULL REFERENCES vehicles.make(id) ON DELETE RESTRICT,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Model availability by year
|
||||
CREATE TABLE IF NOT EXISTS vehicles.model_year (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_id BIGINT NOT NULL REFERENCES vehicles.model(id) ON DELETE RESTRICT,
|
||||
year INTEGER NOT NULL CHECK (year BETWEEN 1950 AND 2100)
|
||||
);
|
||||
|
||||
-- Trims (year-specific)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_year_id BIGINT NOT NULL REFERENCES vehicles.model_year(id) ON DELETE RESTRICT,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Engines (canonical)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.engine (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
code TEXT NULL,
|
||||
displacement_l NUMERIC(3,1) NULL,
|
||||
cylinders SMALLINT NULL,
|
||||
fuel_type TEXT NULL,
|
||||
aspiration TEXT NULL
|
||||
);
|
||||
|
||||
-- Trim to Engine mapping (many-to-many)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim_engine (
|
||||
trim_id BIGINT NOT NULL REFERENCES vehicles.trim(id) ON DELETE RESTRICT,
|
||||
engine_id BIGINT NOT NULL REFERENCES vehicles.engine(id) ON DELETE RESTRICT,
|
||||
PRIMARY KEY (trim_id, engine_id)
|
||||
);
|
||||
|
||||
-- Optional: Transmissions (reserved for future)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.transmission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NULL,
|
||||
gears SMALLINT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim_transmission (
|
||||
trim_id BIGINT NOT NULL REFERENCES vehicles.trim(id) ON DELETE RESTRICT,
|
||||
transmission_id BIGINT NOT NULL REFERENCES vehicles.transmission(id) ON DELETE RESTRICT,
|
||||
PRIMARY KEY (trim_id, transmission_id)
|
||||
);
|
||||
|
||||
-- Optional: Performance (reserved for future)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.performance (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
engine_id BIGINT NULL REFERENCES vehicles.engine(id) ON DELETE SET NULL,
|
||||
trim_id BIGINT NULL REFERENCES vehicles.trim(id) ON DELETE SET NULL,
|
||||
horsepower NUMERIC(6,2) NULL,
|
||||
torque NUMERIC(6,2) NULL,
|
||||
top_speed NUMERIC(6,2) NULL,
|
||||
zero_to_sixty NUMERIC(4,2) NULL
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Uniques and indexes to enforce data integrity and performance
|
||||
|
||||
-- Unique, case-insensitive names
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_make_name ON vehicles.make (lower(name));
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_model_make_name ON vehicles.model (make_id, lower(name));
|
||||
|
||||
-- Model/Year availability
|
||||
ALTER TABLE vehicles.model_year
|
||||
ADD CONSTRAINT ux_model_year UNIQUE (model_id, year);
|
||||
CREATE INDEX IF NOT EXISTS ix_model_year_year_model ON vehicles.model_year (year, model_id);
|
||||
|
||||
-- Trims are unique per model_year by name (case-insensitive)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_trim_modelyear_name ON vehicles.trim (model_year_id, lower(name));
|
||||
CREATE INDEX IF NOT EXISTS ix_trim_modelyear_name ON vehicles.trim (model_year_id, name);
|
||||
|
||||
-- Engine uniqueness (prefer code when present)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_engine_code_not_null ON vehicles.engine (code) WHERE code IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_engine_name ON vehicles.engine (lower(name));
|
||||
|
||||
-- Bridge indexes
|
||||
CREATE INDEX IF NOT EXISTS ix_trim_engine_trim ON vehicles.trim_engine (trim_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_trim_engine_engine ON vehicles.trim_engine (engine_id);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Minimal seed data for testing dropdown hierarchy
|
||||
INSERT INTO vehicles.make (name) VALUES ('Honda') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO vehicles.make (name) VALUES ('Toyota') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Resolve make ids
|
||||
WITH m AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('Honda')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT m.id, 'Civic' FROM m
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH m AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('Toyota')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT m.id, 'Corolla' FROM m
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Model years
|
||||
WITH mo AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Civic')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT mo.id, 2024 FROM mo ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH mo AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Corolla')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT mo.id, 2024 FROM mo ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Trims
|
||||
WITH my AS (
|
||||
SELECT my.id FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Civic') AND my.year = 2024
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, 'LX' FROM my ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH my AS (
|
||||
SELECT my.id FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Corolla') AND my.year = 2024
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, 'LE' FROM my ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Engines
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES ('2.0L I4', 'K20', 2.0, 4, 'Gasoline', 'NA')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES ('2.0L I4', 'M20', 2.0, 4, 'Gasoline', 'NA')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Map engines to trims
|
||||
WITH t AS (
|
||||
SELECT t.id AS trim_id, e.id AS engine_id
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id AND my.year = 2024
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
JOIN vehicles.make ma ON ma.id = mo.make_id
|
||||
JOIN vehicles.engine e ON e.code IN ('K20','M20')
|
||||
WHERE lower(ma.name) = lower('Honda') AND lower(mo.name) = lower('Civic') AND lower(t.name) = lower('LX')
|
||||
)
|
||||
INSERT INTO vehicles.trim_engine (trim_id, engine_id)
|
||||
SELECT trim_id, engine_id FROM t ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Seed sample data based on ETL source filter (etl/sources/makes.json)
|
||||
-- Focus on Chevrolet (Corvette) and GMC (Sierra 1500)
|
||||
|
||||
-- Makes
|
||||
INSERT INTO vehicles.make (name) VALUES ('Chevrolet') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO vehicles.make (name) VALUES ('GMC') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Chevrolet Corvette
|
||||
WITH chevy AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('Chevrolet')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT chevy.id, 'Corvette' FROM chevy
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH corvette AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Corvette')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT corvette.id, 2024 FROM corvette
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Corvette trims (2024)
|
||||
WITH my AS (
|
||||
SELECT my.id
|
||||
FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Corvette') AND my.year = 2024
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, t.name
|
||||
FROM my, (VALUES ('Stingray'), ('Z06')) AS t(name)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Corvette engines
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES
|
||||
('6.2L V8 LT2', 'LT2', 6.2, 8, 'Gasoline', 'NA'),
|
||||
('5.5L V8 LT6', 'LT6', 5.5, 8, 'Gasoline', 'NA')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Map Corvette engines to trims
|
||||
WITH t AS (
|
||||
SELECT t.id AS trim_id, t.name AS trim_name
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id AND my.year = 2024
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id AND lower(mo.name) = lower('Corvette')
|
||||
)
|
||||
INSERT INTO vehicles.trim_engine (trim_id, engine_id)
|
||||
SELECT t.trim_id, e.id
|
||||
FROM t
|
||||
JOIN vehicles.engine e
|
||||
ON (t.trim_name = 'Stingray' AND e.code = 'LT2')
|
||||
OR (t.trim_name = 'Z06' AND e.code = 'LT6')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- GMC Sierra 1500
|
||||
WITH gmc AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('GMC')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT gmc.id, 'Sierra 1500' FROM gmc
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH sierra AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Sierra 1500')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT sierra.id, 2024 FROM sierra
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Sierra trims (2024)
|
||||
WITH my AS (
|
||||
SELECT my.id
|
||||
FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Sierra 1500') AND my.year = 2024
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, t.name
|
||||
FROM my, (VALUES ('SLE'), ('Denali')) AS t(name)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Sierra engines
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES
|
||||
('5.3L V8 L84', 'L84', 5.3, 8, 'Gasoline', 'NA'),
|
||||
('6.2L V8 L87', 'L87', 6.2, 8, 'Gasoline', 'NA')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Map Sierra engines to trims
|
||||
WITH t AS (
|
||||
SELECT t.id AS trim_id, t.name AS trim_name
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id AND my.year = 2024
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id AND lower(mo.name) = lower('Sierra 1500')
|
||||
)
|
||||
INSERT INTO vehicles.trim_engine (trim_id, engine_id)
|
||||
SELECT t.trim_id, e.id
|
||||
FROM t
|
||||
JOIN vehicles.engine e
|
||||
ON (t.trim_name = 'SLE' AND e.code = 'L84')
|
||||
OR (t.trim_name = 'Denali' AND e.code = 'L87')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Seed specific vehicle combinations requested
|
||||
|
||||
-- Ensure makes exist
|
||||
INSERT INTO vehicles.make (name) VALUES ('GMC') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO vehicles.make (name) VALUES ('Chevrolet') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Ensure models exist under their makes
|
||||
WITH m AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('GMC')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT m.id, 'Sierra 1500' FROM m
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH m AS (
|
||||
SELECT id FROM vehicles.make WHERE lower(name) = lower('Chevrolet')
|
||||
)
|
||||
INSERT INTO vehicles.model (make_id, name)
|
||||
SELECT m.id, 'Corvette' FROM m
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Model years
|
||||
WITH mo AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Sierra 1500')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT mo.id, 2023 FROM mo ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH mo AS (
|
||||
SELECT id FROM vehicles.model WHERE lower(name) = lower('Corvette')
|
||||
)
|
||||
INSERT INTO vehicles.model_year (model_id, year)
|
||||
SELECT mo.id, 2017 FROM mo ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Trims
|
||||
WITH my AS (
|
||||
SELECT my.id FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Sierra 1500') AND my.year = 2023
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, 'AT4x' FROM my ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH my AS (
|
||||
SELECT my.id FROM vehicles.model_year my
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
WHERE lower(mo.name) = lower('Corvette') AND my.year = 2017
|
||||
)
|
||||
INSERT INTO vehicles.trim (model_year_id, name)
|
||||
SELECT my.id, 'Z06 Convertible' FROM my ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Engines (ensure canonical engines exist)
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES ('6.2L V8 L87', 'L87', 6.2, 8, 'Gasoline', 'NA')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)
|
||||
VALUES ('6.2L V8 LT4', 'LT4', 6.2, 8, 'Gasoline', 'SC')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Map engines to trims
|
||||
WITH t AS (
|
||||
SELECT t.id AS trim_id, t.name AS trim_name, my.year, mo.name AS model_name
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id
|
||||
JOIN vehicles.model mo ON mo.id = my.model_id
|
||||
)
|
||||
INSERT INTO vehicles.trim_engine (trim_id, engine_id)
|
||||
SELECT t.trim_id, e.id
|
||||
FROM t
|
||||
JOIN vehicles.engine e
|
||||
ON (t.model_name = 'Sierra 1500' AND t.year = 2023 AND t.trim_name = 'AT4x' AND e.code = 'L87')
|
||||
OR (t.model_name = 'Corvette' AND t.year = 2017 AND t.trim_name = 'Z06 Convertible' AND e.code = 'LT4')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
Reference in New Issue
Block a user