Docs Cleanup

This commit is contained in:
Eric Gullickson
2025-11-03 16:12:29 -06:00
parent 2cc9cc5f9f
commit d8d0ada83f
33 changed files with 158 additions and 2102 deletions

View File

@@ -1,437 +0,0 @@
# Platform Integration - Deployment Ready
## Executive Summary
Successfully integrated the external mvp-platform Python service into the backend as a TypeScript feature module. The application now runs with **5 containers instead of 6**, with all platform logic accessible via unified `/api/platform/*` endpoints.
**Status**: CODE COMPLETE - Ready for container testing and deployment
## What Changed
### Architecture
- **Before**: 6 containers (Traefik, Frontend, Backend, PostgreSQL, Redis, **Platform**)
- **After**: 5 containers (Traefik, Frontend, Backend, PostgreSQL, Redis)
- **Reduction**: -16.7% container count, simplified deployment
### Key Migrations
- VIN decoding: External HTTP service → Internal platform feature
- Vehicle data lookups: External HTTP service → Internal platform feature
- API endpoints: Various locations → Unified `/api/platform/*`
- Technology stack: Python FastAPI → TypeScript Fastify
## Implementation Details
### Wave 1: Foundation (4 agents in parallel)
#### Agent 1: Platform Feature Creator
**Files Created**: 14 files
- Complete feature structure: `backend/src/features/platform/`
- API layer: Routes, controller with JWT authentication
- Domain layer: VIN decode service, vehicle data service, caching
- Data layer: PostgreSQL repository, vPIC client
- Tests: Unit and integration tests
- Documentation: Comprehensive README
**Key Endpoints**:
- `GET /api/platform/years`
- `GET /api/platform/makes?year={year}`
- `GET /api/platform/models?year={year}&make_id={id}`
- `GET /api/platform/trims?year={year}&model_id={id}`
- `GET /api/platform/engines?year={year}&trim_id={id}`
- `GET /api/platform/vehicle?vin={vin}` (VIN decode)
#### Agent 2: VIN Migration - Backend
**Files Modified**: 3 files
**Files Deleted**: 4 directories/files
- Migrated VIN decode from vehicles feature to platform feature
- Updated vehicles service: Uses `getVINDecodeService()` from platform
- Removed external platform client
- Removed vPIC client (moved to platform)
- Updated tests to mock platform service
#### Agent 3: VIN Migration - Frontend
**Files Modified**: 2 files
- Updated API client: All calls now use `/api/platform/*`
- VIN decode: Changed to `GET /api/platform/vehicle?vin=X`
- Mobile enhancements: 44px touch targets, 16px fonts (no iOS zoom)
- Dual workflow: VIN decode OR manual dropdown selection
#### Agent 4: Configuration Cleanup
**Files Modified**: 4 files
- Removed mvp-platform service from docker-compose.yml
- Removed PLATFORM_VEHICLES_API_URL environment variable
- Cleaned platform configuration from production.yml
- Updated Makefile: 6-container → 5-container architecture
### Wave 2: Integration & Documentation (2 agents)
#### Wave 2 Manual Tasks (Agent limits reached)
**Integration Verification**:
- Confirmed vehicles service integration with platform feature (vehicles.service.ts:46, 229)
- Confirmed platform routes registered in app.ts (app.ts:22, 110)
- Confirmed VIN decode workflow intact
**Documentation Updates**:
- README.md: Updated to 5 containers
- CLAUDE.md: Updated architecture description
- docs/README.md: Added platform feature, updated container count
- AI-INDEX.md: Updated to 5-container stack
**New Documentation**:
- `docs/PLATFORM-INTEGRATION-MIGRATION.md`: Complete migration notes
- `docs/PLATFORM-INTEGRATION-TESTING.md`: Comprehensive testing guide
- `backend/src/features/platform/README.md`: Platform feature documentation (created by Agent 1)
### Wave 3: Deployment Preparation
**Archive Platform Service**:
- Created: `archive/platform-services/`
- Moved: Python vehicles service to archive
- Created: Archive README with restoration instructions
- Status: Safe to delete after 30 days in production
## Files Summary
### Created (16 files total)
**Backend Platform Feature** (14 files):
- `backend/src/features/platform/index.ts`
- `backend/src/features/platform/README.md`
- `backend/src/features/platform/api/platform.controller.ts`
- `backend/src/features/platform/api/platform.routes.ts`
- `backend/src/features/platform/domain/vin-decode.service.ts`
- `backend/src/features/platform/domain/vehicle-data.service.ts`
- `backend/src/features/platform/domain/platform-cache.service.ts`
- `backend/src/features/platform/data/vehicle-data.repository.ts`
- `backend/src/features/platform/data/vpic-client.ts`
- `backend/src/features/platform/models/requests.ts`
- `backend/src/features/platform/models/responses.ts`
- `backend/src/features/platform/tests/unit/vin-decode.service.test.ts`
- `backend/src/features/platform/tests/unit/vehicle-data.service.test.ts`
- `backend/src/features/platform/tests/integration/platform.integration.test.ts`
**Documentation** (2 files):
- `docs/PLATFORM-INTEGRATION-MIGRATION.md`
- `docs/PLATFORM-INTEGRATION-TESTING.md`
- `archive/platform-services/README.md`
- `DEPLOYMENT-READY.md` (this file)
### Modified (10 files)
**Configuration**:
- `docker-compose.yml` - Removed mvp-platform service
- `.env` - Removed platform URL
- `config/app/production.yml` - Removed platform config
- `Makefile` - Updated to 5-container architecture
**Backend**:
- `backend/src/app.ts` - Registered platform routes
- `backend/src/features/vehicles/domain/vehicles.service.ts` - Uses platform VIN decode
- `backend/src/features/vehicles/tests/unit/vehicles.service.test.ts` - Updated mocks
**Frontend**:
- `frontend/src/features/vehicles/api/vehicles.api.ts` - Updated endpoints
- `frontend/src/features/vehicles/components/VehicleForm.tsx` - Mobile enhancements
**Documentation**:
- `README.md` - Updated to 5 containers
- `CLAUDE.md` - Updated architecture
- `docs/README.md` - Added platform feature
- `AI-INDEX.md` - Updated architecture description
### Deleted (4 locations)
- `backend/src/features/vehicles/external/platform-vehicles/` - Old external client
- `backend/src/features/vehicles/domain/platform-integration.service.ts` - Wrapper service
- `backend/src/features/vehicles/external/vpic/` - Moved to platform
- `backend/src/features/vehicles/tests/unit/vpic.client.test.ts` - Moved to platform tests
### Archived (1 location)
- `mvp-platform-services/vehicles/``archive/platform-services/vehicles/`
## Technical Highlights
### VIN Decode Strategy
Multi-tier resilience:
1. **Redis Cache**: 7-day TTL for success, 1-hour for failures
2. **PostgreSQL**: `vehicles.f_decode_vin()` function for high-confidence decode
3. **vPIC API**: NHTSA fallback via circuit breaker (opossum)
4. **Graceful Degradation**: Meaningful errors when all sources fail
### Circuit Breaker Configuration
- **Timeout**: 6 seconds
- **Error Threshold**: 50%
- **Reset Timeout**: 30 seconds
- **Monitoring**: State transition logging
### Caching Strategy
- **Vehicle Data**: 6-hour TTL (mvp:platform:vehicle-data:*)
- **VIN Decode**: 7-day TTL success, 1-hour failure (mvp:platform:vin-decode:*)
- **Graceful Fallback**: Continues if Redis unavailable
### Security
- **Authentication**: JWT required on all `/api/platform/*` endpoints
- **Input Validation**: Zod schemas for all requests
- **SQL Safety**: Prepared statements via node-postgres
- **Error Handling**: No internal details exposed to clients
## Pre-Deployment Checklist
### Code Quality
- TypeScript compilation: Needs verification in containers
- Linter: Needs verification in containers
- Tests: Need to run in Docker (see PLATFORM-INTEGRATION-TESTING.md)
- Git: Changes committed (ready for commit)
### Integration Points
- Platform routes registered: app.ts:22, 110
- Vehicles service integration: vehicles.service.ts:46, 229
- Frontend API calls: Updated to /api/platform/*
- Docker compose: 5 services defined, validated with `docker compose config`
### Documentation
- Architecture docs: Updated to 5 containers
- API documentation: Complete in platform/README.md
- Migration notes: docs/PLATFORM-INTEGRATION-MIGRATION.md
- Testing guide: docs/PLATFORM-INTEGRATION-TESTING.md
- Archive README: Restoration instructions documented
## Deployment Steps
### 1. Start Docker
```bash
# Ensure Docker is running
docker --version
# Verify docker-compose.yml
docker compose config
```
### 2. Rebuild Containers
```bash
# Rebuild all containers with new code
make rebuild
# Or manually:
docker compose down
docker compose build
docker compose up -d
```
### 3. Verify Container Count
```bash
# Should show exactly 5 services
docker compose ps
# Expected:
# mvp-traefik - running
# mvp-frontend - running
# mvp-backend - running
# mvp-postgres - running
# mvp-redis - running
```
### 4. Run Tests
```bash
# TypeScript compilation
docker compose exec mvp-backend npm run type-check
# Linter
docker compose exec mvp-backend npm run lint
# Platform unit tests
docker compose exec mvp-backend npm test -- features/platform/tests/unit
# Platform integration tests
docker compose exec mvp-backend npm test -- features/platform/tests/integration
# Vehicles integration tests
docker compose exec mvp-backend npm test -- features/vehicles/tests/integration
```
### 5. Test Endpoints
```bash
# Health check
curl http://localhost:3001/health
# Expected: {"status":"healthy","features":["vehicles","documents","fuel-logs","stations","maintenance","platform"]}
# With valid JWT token:
TOKEN="your-jwt-token"
# Test years endpoint
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3001/api/platform/years
# Test VIN decode
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3001/api/platform/vehicle?vin=1HGCM82633A123456
```
### 6. Frontend Verification
```bash
# Access frontend
open https://motovaultpro.com
# Test workflows:
# 1. Navigate to Vehicles → Add Vehicle
# 2. Test VIN decode: Enter VIN, click "Decode VIN"
# 3. Test manual selection: Select year → make → model
# 4. Test on mobile (Chrome DevTools responsive mode)
```
### 7. Monitor Logs
```bash
# Watch all logs
make logs
# Or specific services:
make logs-backend | grep platform
make logs-frontend
# Monitor for errors during testing
```
### 8. Performance Check
```bash
# Test cache performance
time curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3001/api/platform/years
# First call: < 500ms (cache miss)
# Second call: < 100ms (cache hit)
```
### 9. Redis Cache Verification
```bash
# Connect to Redis
docker compose exec mvp-redis redis-cli
# Check platform cache keys
KEYS mvp:platform:*
# Check TTL
TTL mvp:platform:years
TTL mvp:platform:vin-decode:1HGCM82633A123456
```
### 10. Create Git Tag
```bash
# After all tests pass
git add .
git commit -m "Platform Integration Complete - 5 Container Architecture
- Integrated mvp-platform Python service into backend as TypeScript platform feature
- Reduced container count from 6 to 5
- Migrated VIN decode and vehicle data lookups to /api/platform/*
- Updated frontend to use unified platform endpoints
- Enhanced mobile responsiveness (44px touch targets, 16px fonts)
- Comprehensive testing guide and migration documentation
Wave 1: Platform Feature Creator, VIN Migration (Backend/Frontend), Configuration Cleanup
Wave 2: Integration Verification, Documentation Updates
Wave 3: Archive Platform Service, Deployment Preparation"
git tag v1.0-platform-integrated
```
## Rollback Plan
If critical issues discovered:
```bash
# 1. Restore docker-compose.yml
git restore docker-compose.yml
# 2. Restore platform service
mv archive/platform-services/vehicles mvp-platform-services/
# 3. Rebuild containers
docker compose down
docker compose up -d
# 4. Revert code changes
git revert HEAD
```
## Success Metrics
Post-deployment monitoring (24 hours):
- Container count: 5 (down from 6)
- All automated tests: Passing
- VIN decode response time: <500ms
- Dropdown response time: <100ms
- Redis cache hit rate: >80%
- Zero errors in logs
- Mobile + desktop: Both workflows functional
- TypeScript compilation: Zero errors
- Linter: Zero issues
## Known Limitations
### Testing Status
- Tests created but not yet executed (requires Docker)
- TypeScript compilation not yet verified in containers
- Integration tests not yet run in containers
### User Testing Needed
- VIN decode workflow: Not yet tested end-to-end
- Dropdown cascade: Not yet tested in browser
- Mobile responsiveness: Not yet tested on devices
### Performance
- Cache hit rates: Not yet measured
- Response times: Not yet benchmarked
- Circuit breaker: Not yet tested under load
## Next Steps
1. **Start Docker and run tests** (BLOCKING)
- See: `docs/PLATFORM-INTEGRATION-TESTING.md`
- Required before deployment
2. **Fix any test failures**
- TypeScript errors
- Linter issues
- Test failures
3. **End-to-end testing**
- VIN decode workflow
- Dropdown cascade
- Mobile + desktop
4. **Performance verification**
- Response time benchmarks
- Cache hit rate measurement
- Circuit breaker testing
5. **Production deployment**
- Create git tag
- Deploy to production
- Monitor logs for 24 hours
6. **Archive cleanup** (after 30 days)
- Verify platform feature stable
- Delete archived Python service
- Update documentation
## Documentation Index
- **Migration Notes**: `docs/PLATFORM-INTEGRATION-MIGRATION.md`
- **Testing Guide**: `docs/PLATFORM-INTEGRATION-TESTING.md`
- **Platform Feature**: `backend/src/features/platform/README.md`
- **Archive Instructions**: `archive/platform-services/README.md`
- **This File**: `DEPLOYMENT-READY.md`
## Support
For issues or questions:
1. Review `docs/PLATFORM-INTEGRATION-TESTING.md` for troubleshooting
2. Check backend logs: `make logs-backend | grep platform`
3. Review platform feature: `backend/src/features/platform/README.md`
4. Consult migration notes: `docs/PLATFORM-INTEGRATION-MIGRATION.md`
---
**Platform Integration Status**: CODE COMPLETE
All code changes implemented and documented. Ready for Docker container testing and deployment verification.
Next action: Start Docker and run tests per `docs/PLATFORM-INTEGRATION-TESTING.md`

View File

@@ -1,88 +0,0 @@
# 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.

View File

@@ -1,44 +0,0 @@
# 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.

View File

@@ -1,50 +0,0 @@
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()

View File

@@ -1,40 +0,0 @@
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

View File

@@ -1,202 +0,0 @@
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"
)

View File

@@ -1,84 +0,0 @@
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

View File

@@ -1,79 +0,0 @@
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]

View File

@@ -1,116 +0,0 @@
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}"
)
)

View File

@@ -1,110 +0,0 @@
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"
)

View File

@@ -1,88 +0,0 @@
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)}

View File

@@ -1,58 +0,0 @@
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

View File

@@ -1,30 +0,0 @@
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"]

View File

@@ -1,69 +0,0 @@
{
"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"
]
}

View File

@@ -1,6 +0,0 @@
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

View File

@@ -1,73 +0,0 @@
-- 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
);

View File

@@ -1,23 +0,0 @@
-- 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);

View File

@@ -1,70 +0,0 @@
-- 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;

View File

@@ -1,105 +0,0 @@
-- 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;

View File

@@ -1,75 +0,0 @@
-- 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;

View File

@@ -1,23 +0,0 @@
# MotoVaultPro platform services ConfigMap template
tenants_service:
database:
host: platform-postgres
port: 5432
name: platform
user: platform_user
auth0:
domain: motovaultpro.us.auth0.com
audience: https://api.motovaultpro.com
vehicles_service:
database:
host: mvp-platform-vehicles-db
port: 5432
name: vehicles
user: mvp_platform_user
redis:
host: mvp-platform-vehicles-redis
port: 6379
cors_allow_origins:
- https://admin.motovaultpro.com
- https://motovaultpro.com

View File

@@ -41,12 +41,6 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
+------------------+ +--------------------+ +------------------+ +--------------------+
(frontend network) (backend network) (frontend network) (backend network)
| |
v
+-------------------+
| Platform |
| Python + FastAPI |
| Port: 8000 |
+-------------------+
| |
+---------------------------------+ +---------------------------------+
| | | |
@@ -103,14 +97,12 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
7. Backend → Response → Frontend 7. Backend → Response → Frontend
``` ```
### Backend to Platform Flow ### Backend Platform Flow
``` ```
1. Backend Feature → HTTP Request → Platform Service 1. Backend Feature → Calls platform domain services (TypeScript in-process)
- URL: http://mvp-platform:8000 2. Platform module → Query PostgreSQL (vehicles schema)
- Headers: Authorization: Bearer <API_KEY> 3. Platform module → Check/Update Redis cache
2. Platform → Query PostgreSQL (vehicles schema) 4. Platform response → Returned to caller (vehicles, frontend proxies, etc.)
3. Platform → Check/Update Redis Cache
4. Platform → Response → Backend
``` ```
### Data Access Flow ### Data Access Flow
@@ -196,53 +188,51 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
- Session storage (future) - Session storage (future)
- Rate limiting (future) - Rate limiting (future)
### Platform (Vehicle Data Service) ### Platform Module (Vehicle Data)
- **Technology**: Python 3.11 + FastAPI - **Technology**: TypeScript feature capsule inside `backend/src/features/platform/`
- **Build**: Custom Dockerfile from `mvp-platform-services/vehicles/` - **Runtime**: Shares the `mvp-backend` Fastify container
- **Port**: 8000 (internal) - **Dependencies**: PostgreSQL, Redis (accessed via backend connection pool)
- **Networks**: backend, database - **Deployment**: Bundled with backend build; no separate container or port
- **Dependencies**: PostgreSQL, Redis - **Health**: Covered by backend `/health` endpoint and feature-specific logs
- **Health Check**: `wget http://localhost:8000/health` (30s interval)
- **Configuration**: - **Configuration**:
- `/app/config/production.yml` - Service config - `backend/src/features/platform/domain/*.ts` - Business logic
- `/app/config/shared.yml` - Shared config - `backend/src/features/platform/data/*.ts` - Database + vPIC integration
- **Secrets**: - **Secrets**:
- `postgres-password` - Shared database access - Reuses backend secrets (PostgreSQL, Auth0, etc.)
- **Purpose**: - **Purpose**:
- Vehicle make/model/trim/engine data - Vehicle make/model/trim/engine data
- VIN decoding (planned) - VIN decoding (planned)
- Standardized vehicle information - Standardized vehicle information
## Platform Service Integration ## Platform Module Integration
### Current Architecture ### Current Architecture
The Platform service is a **separate Python container** that provides vehicle data capabilities to the backend. This separation exists for: Platform capabilities now run **in-process** within the backend as a feature capsule. This delivers:
- **Technology Independence**: Python ecosystem for data processing - **Shared Language & Tooling**: TypeScript across the stack
- **Specialized Caching**: Year-based hierarchical caching strategy - **Centralized Caching**: Reuses backend Redis helpers for year-based lookups
- **Resource Isolation**: Independent scaling and monitoring - **Simplified Operations**: No extra container to build, scale, or monitor
### Shared Infrastructure ### Shared Infrastructure
- **Database**: Both Backend and Platform use `mvp-postgres` - **Database**: Uses the backend connection pool against `mvp-postgres`
- **Cache**: Both services share `mvp-redis` - **Cache**: Shares the backend Redis client (`mvp-redis`)
- **Network**: Connected via `backend` and `database` networks - **Runtime**: Packaged with the `mvp-backend` container and Fastify plugins
- **Secrets**: Share database credentials - **Secrets**: Relies on the same secret files as the backend (PostgreSQL, Auth0, etc.)
### Communication Pattern ### Communication Pattern
```javascript ```typescript
// Backend calls Platform via internal HTTP // Vehicles feature pulling platform data via domain services
const response = await fetch('http://mvp-platform:8000/api/v1/vehicles/makes?year=2024', { const platformService = new PlatformVehicleDataService({
headers: { db: postgresPool,
'Authorization': `Bearer ${platformApiKey}` cache: redisClient
}
}); });
const makes = await platformService.getMakes({ year: 2024 });
``` ```
### Future Evolution ### Ongoing Work
**Planned**: Absorb Platform service into Backend as a feature capsule - Continue migrating remaining FastAPI utilities into TypeScript where needed
- Timeline: Post-MVP phase - Expand `/api/platform/*` endpoints as new lookup types are added
- Approach: Rewrite Python service in Node.js - Monitor backend logs for `platform` namespace entries to verify health
- Location: `backend/src/features/vehicle-platform/`
- Benefits: Simplified deployment, shared codebase, reduced containers
## Feature Capsule Architecture ## Feature Capsule Architecture
@@ -255,7 +245,8 @@ backend/src/features/
├── fuel-logs/ # Fuel tracking ├── fuel-logs/ # Fuel tracking
├── maintenance/ # Service records ├── maintenance/ # Service records
├── stations/ # Gas station locations ├── stations/ # Gas station locations
── documents/ # Document storage ── documents/ # Document storage
└── platform/ # Vehicle data + VIN decoding
``` ```
### Feature Structure ### Feature Structure
@@ -372,7 +363,6 @@ The application uses Kubernetes-inspired configuration patterns:
**Secrets** (File-based): **Secrets** (File-based):
- `/run/secrets/postgres-password` - Database credentials - `/run/secrets/postgres-password` - Database credentials
- `/run/secrets/platform-vehicles-api-key` - Service auth
- `/run/secrets/auth0-client-secret` - OAuth credentials - `/run/secrets/auth0-client-secret` - OAuth credentials
- `/run/secrets/google-maps-api-key` - External API keys - `/run/secrets/google-maps-api-key` - External API keys
@@ -384,14 +374,13 @@ CONFIG_PATH: /app/config/production.yml
SECRETS_DIR: /run/secrets SECRETS_DIR: /run/secrets
DATABASE_HOST: mvp-postgres DATABASE_HOST: mvp-postgres
REDIS_HOST: mvp-redis REDIS_HOST: mvp-redis
PLATFORM_VEHICLES_API_URL: http://mvp-platform:8000
``` ```
## Development Workflow ## Development Workflow
### Container Commands (via Makefile) ### Container Commands (via Makefile)
```bash ```bash
make start # Start all 6 containers make start # Start all 5 containers
make stop # Stop all containers make stop # Stop all containers
make restart # Restart all containers make restart # Restart all containers
make rebuild # Rebuild and restart containers make rebuild # Rebuild and restart containers
@@ -407,12 +396,10 @@ docker ps
# View specific service logs # View specific service logs
docker logs mvp-backend -f docker logs mvp-backend -f
docker logs mvp-platform -f
docker logs mvp-postgres -f docker logs mvp-postgres -f
# Test health endpoints # Test health endpoints
curl http://localhost:3001/health # Backend curl http://localhost:3001/health # Backend (includes platform module)
curl http://localhost:8000/health # Platform
``` ```
### Database Access ### Database Access

View File

@@ -1,13 +1,13 @@
# MVP Platform Service # MVP Platform Module
## Overview ## Overview
The MVP Platform service is an **integrated service** that provides platform capabilities to the MotoVaultPro application. This service is part of the simplified 6-container architecture. The MVP Platform module is fully integrated inside the MotoVaultPro backend container. It delivers all platform capabilities without requiring a separate service or container in the simplified five-container stack.
## Architecture ## Architecture
The platform service is integrated into the main application stack: The platform module runs as part of the backend service:
- **Service Container**: mvp-platform - **Runtime**: `mvp-backend` container (Fastify API)
- **Shared Database**: Uses mvp-postgres - **Shared Database**: Uses mvp-postgres
- **Shared Cache**: Uses mvp-redis - **Shared Cache**: Uses mvp-redis
@@ -37,18 +37,17 @@ The platform provides vehicle data capabilities including:
**Start All Services**: **Start All Services**:
```bash ```bash
make start # Starts all 6 containers make start # Starts the five-container stack
``` ```
**Service Logs**: **Backend Logs (includes platform module)**:
```bash ```bash
make logs # All service logs make logs-backend
docker logs mvp-platform
``` ```
**Service Shell Access**: **Backend Shell (platform code lives here)**:
```bash ```bash
docker exec -it mvp-platform sh make shell-backend
``` ```
### Database Management ### Database Management
@@ -66,10 +65,10 @@ make db-shell-app
## Deployment Strategy ## Deployment Strategy
### Integrated Deployment ### Integrated Deployment
The platform service deploys with the main application: The platform module deploys with the main application:
- Same deployment pipeline - Same deployment pipeline
- Shares database and cache - Shares database and cache
- Deployed as part of 6-container stack - Deployed as part of the five-container stack
## Integration Patterns ## Integration Patterns
@@ -85,7 +84,7 @@ Application features access platform data through shared database:
**Service Discovery Problems**: **Service Discovery Problems**:
- Verify Docker networking: `docker network ls` - Verify Docker networking: `docker network ls`
- Check container connectivity: `docker exec -it mvp-platform sh` - Check backend container connectivity: `docker compose exec mvp-backend sh`
**Database Connection Issues**: **Database Connection Issues**:
- Verify mvp-postgres is healthy - Verify mvp-postgres is healthy
@@ -93,19 +92,19 @@ Application features access platform data through shared database:
### Health Checks ### Health Checks
**Verify Platform Service**: **Verify Backend Service (contains platform module)**:
```bash ```bash
docker ps | grep mvp-platform docker compose ps mvp-backend
``` ```
### Logs and Debugging ### Logs and Debugging
**Service Logs**: **Service Logs**:
```bash ```bash
docker logs mvp-platform --tail=100 -f docker compose logs -f mvp-backend
``` ```
**Database Logs**: **Database Logs**:
```bash ```bash
docker logs mvp-postgres --tail=100 -f docker compose logs -f mvp-postgres
``` ```

View File

@@ -14,11 +14,11 @@ MotoVaultPro uses a 4-agent team for optimal development velocity and quality en
- Platform service integration - Platform service integration
- Backend tests and migrations - Backend tests and migrations
**Platform Service Agent** - Independent microservices **Platform Feature Agent** - Integrated platform module
- Building new platform services in `mvp-platform-services/{service}/` - Enhancing `backend/src/features/platform/`
- FastAPI microservice development - Vehicle lookup + VIN decoding logic
- ETL pipelines and service databases - Redis/SQL performance tuning for platform endpoints
- Platform service tests and deployment - Platform module tests and documentation
**Mobile-First Frontend Agent** - Responsive UI/UX **Mobile-First Frontend Agent** - Responsive UI/UX
- React components in `frontend/src/features/{feature}/` - React components in `frontend/src/features/{feature}/`
@@ -45,10 +45,10 @@ Task: "Build responsive UI for {feature}. Read backend API docs and implement mo
Test on 320px and 1920px viewports." Test on 320px and 1920px viewports."
Agent: Mobile-First Frontend Agent Agent: Mobile-First Frontend Agent
# Platform microservice # Platform module work
Task: "Create {service} platform microservice with FastAPI. Task: "Enhance platform feature capsule (backend/src/features/platform).
Implement API, database, and health checks with tests." Implement API/domain/data changes with accompanying tests."
Agent: Platform Service Agent Agent: Platform Feature Agent
# Quality validation # Quality validation
Task: "Validate {feature} quality gates. Run all tests, check linting, verify mobile + desktop. Task: "Validate {feature} quality gates. Run all tests, check linting, verify mobile + desktop.
@@ -109,11 +109,12 @@ Agent: Quality Enforcer Agent
- Database: `backend/src/features/{feature}/data/` - Database: `backend/src/features/{feature}/data/`
- After changes: `make rebuild` then check logs - After changes: `make rebuild` then check logs
### Platform Service Changes (Python) ### Platform Module Changes (TypeScript)
**Agent**: Platform Service Agent **Agent**: Platform Feature Agent
- Service: `mvp-platform-services/{service}/` - Feature capsule: `backend/src/features/platform/`
- API: `mvp-platform-services/{service}/api/` - API routes: `backend/src/features/platform/api/`
- After changes: `make rebuild` then check service health - Domain/data: `backend/src/features/platform/domain/` and `data/`
- After changes: `make rebuild` then verify platform endpoints via backend logs/tests
### Database Changes ### Database Changes
- Add migration: `backend/src/features/{feature}/migrations/00X_description.sql` - Add migration: `backend/src/features/{feature}/migrations/00X_description.sql`
@@ -159,12 +160,12 @@ Agent: Quality Enforcer Agent
4. Ensure touch targets are 44px minimum 4. Ensure touch targets are 44px minimum
5. Validate keyboard navigation on desktop 5. Validate keyboard navigation on desktop
### Add Platform Service Integration ### Add Platform Integration
**Agents**: Platform Service Agent + Feature Capsule Agent **Agents**: Platform Feature Agent + Feature Capsule Agent
1. Platform Service Agent: Implement service endpoint 1. Platform Feature Agent: Implement/update platform endpoint logic
2. Feature Capsule Agent: Create client in `external/platform-{service}/` 2. Feature Capsule Agent: Update consuming feature client (e.g. `external/platform-vehicles/`)
3. Feature Capsule Agent: Add circuit breaker and caching 3. Feature Capsule Agent: Adjust caching/circuit breaker strategies as needed
4. Test integration with both agents 4. Joint testing: run targeted unit/integration suites
5. Quality Enforcer Agent: Validate end-to-end 5. Quality Enforcer Agent: Validate end-to-end
### Run Quality Checks ### Run Quality Checks
@@ -188,9 +189,9 @@ Code is complete when:
## Architecture Quick Reference ## Architecture Quick Reference
### Hybrid Platform ### Application Stack
- **Platform Microservices**: Independent services in `mvp-platform-services/` - **Backend Feature Capsules**: Modular monolith in `backend/src/features/`
- **Application Features**: Modular monolith in `backend/src/features/` - **Platform Module**: Vehicle data + VIN decoding in `backend/src/features/platform/`
- **Frontend**: React SPA in `frontend/src/` - **Frontend**: React SPA in `frontend/src/`
### Feature Capsule Pattern ### Feature Capsule Pattern
@@ -206,29 +207,21 @@ backend/src/features/{feature}/
└── tests/ # Unit + integration tests └── tests/ # Unit + integration tests
``` ```
### Platform Service Pattern
Each service is independent:
```
mvp-platform-services/{service}/
├── api/ # FastAPI application
├── database/ # SQLAlchemy models + migrations
└── tests/ # Service tests
```
## Important Context ## Important Context
- **Auth**: Frontend uses Auth0, backend validates JWTs - **Auth**: Frontend uses Auth0, backend validates JWTs
- **Database**: PostgreSQL with user-isolated data (user_id scoping) - **Database**: PostgreSQL with user-isolated data (user_id scoping)
- **Platform APIs**: Authenticated via API keys (service-to-service) - **Platform APIs**: Exposed via `/api/platform/*`, secured with Auth0 JWTs
- **Caching**: Redis with feature-specific TTL strategies - **Caching**: Redis with feature-specific TTL strategies
- **Testing**: Jest (backend/frontend), pytest (platform services) - **Testing**: Jest (backend + frontend)
- **Docker-First**: All development in containers (production-only) - **Docker-First**: All development in containers (production-only)
## Agent Coordination Rules ## Agent Coordination Rules
### Clear Ownership ### Clear Ownership
- Feature Capsule Agent: Backend application features - Feature Capsule Agent: Backend application features
- Platform Service Agent: Independent microservices - Platform Feature Agent: Platform capsule inside backend
- Mobile-First Frontend Agent: All UI/UX code - Mobile-First Frontend Agent: All UI/UX code
- Quality Enforcer Agent: Testing and validation only - Quality Enforcer Agent: Testing and validation only
@@ -264,4 +257,4 @@ mvp-platform-services/{service}/
- Testing: `docs/TESTING.md` - Testing: `docs/TESTING.md`
- Context Loading: `.ai/context.json` - Context Loading: `.ai/context.json`
- Development Guidelines: `CLAUDE.md` - Development Guidelines: `CLAUDE.md`
- Feature Documentation: `backend/src/features/{feature}/README.md` - Feature Documentation: `backend/src/features/{feature}/README.md`

View File

@@ -22,5 +22,58 @@ Project documentation hub for the 5-container single-tenant architecture with in
## Notes ## Notes
- Canonical URLs: Frontend `https://motovaultpro.com`, Backend health `http://localhost:3001/health`. - Canonical URLs: Frontend `https://motovaultpro.com`, Backend health `http://localhost:3001/health`.
- Hosts entry required: `127.0.0.1 motovaultpro.com`.
- Feature test coverage: Basic test structure exists for vehicles and documents features; other features have placeholder tests. - Feature test coverage: Basic test structure exists for vehicles and documents features; other features have placeholder tests.
## Cleanup Notes
> Documentation Audit
- Documented commands make test/make test-frontend appear across README.md:12-17, backend/README.md:20-38, docs/TESTING.md:24-49, AI-INDEX.md:8, and frontend/
README.md:8-28; the Makefile only advertises them in help (Makefile:11-12) with no corresponding targets, so the instructions currently break.
- README.md:27 and AI-INDEX.md:19 point folks to http://localhost:3001/health, but docker-compose.yml:77-135 never exposes that port, meaning the reachable
probe is https://motovaultpro.com/api/health via Traefik.
- docs/TESTING.md:11-99,169-175 commit to full per-feature suites and fixtures such as vehicles.fixtures.json, yet backend/src/features/fuel-logs/tests and
backend/src/features/maintenance/tests contain no files (see find output), and backend/src/features/vehicles/tests/fixtures is empty.
- Backend fuel-log docs still describe the legacy contract (gallons, pricePerGallon, mpg field) in backend/src/features/fuel-logs/README.md:20-78, but the
code now accepts/returns dateTime, fuelUnits, costPerUnit, efficiency, etc. (backend/src/features/fuel-logs/domain/fuel-logs.service.ts:17-320).
Security & Platform
- docs/VEHICLES-API.md:35-36 and 149-151 still require an API key, while the platform routes enforce Auth0 JWTs via fastify.authenticate (backend/src/
features/platform/api/platform.routes.ts:20-42); theres no API key configuration in the repo.
- docs/VEHICLES-API.md:38-41 promises 1-hour Redis TTLs, but PlatformCacheService stores dropdown data for six hours and successful VIN decodes for seven days
(backend/src/features/platform/domain/platform-cache.service.ts:27-110).
- docs/SECURITY.md:15-16 claims “Unauthenticated Endpoints None,” yet /health and /api/health are open (backend/src/app.ts:69-86); docs/SECURITY.md:25-
29 also states Postgres connections are encrypted even though the pool uses a plain postgresql:// URL without SSL options (backend/src/core/config/config-
loader.ts:213-218; backend/src/core/config/database.ts:1-16).
- docs/SECURITY.md:21-23 references the old FastAPI VIN service, but VIN decoding now lives entirely in TypeScript (backend/src/features/platform/domain/vin-
decode.service.ts:1-114).
Feature Guides
- backend/src/features/vehicles/README.md:15-108 still references an implemented dropdown proxy, a platform-vehicles client folder, and a platform-
vehicles.client.test.ts, yet the service methods remain TODO stubs returning empty arrays (backend/src/features/vehicles/domain/vehicles.service.ts:165-193)
and there is no such client or test file in the tree.
- docs/VEHICLES-API.md:58-97 says the frontend consumes /api/vehicles/dropdown/*, but the current client hits /platform/* and expects raw arrays (frontend/
src/features/vehicles/api/vehicles.api.ts:35-69) while the backend responds with wrapped objects like { makes: [...] } (backend/src/features/platform/api/
platform.controller.ts:48-94), so either the docs or the code path needs realignment.
- backend/src/features/fuel-logs/README.md:150-153 advertises a fuel-stats:vehicle:{vehicleId} Redis cache and response fields like totalLogs/averageMpg, but
getVehicleStats performs fresh queries and returns { logCount, totalFuelUnits, totalCost, averageCostPerUnit, totalDistance, averageEfficiency, unitLabels }
with no caching (backend/src/features/fuel-logs/domain/fuel-logs.service.ts:226-320).
- backend/src/features/documents/README.md:4,23-25 describes S3-compatible storage and a core/middleware/user-context dependency; in reality uploads go to
the filesystem adapter (backend/src/core/storage/storage.service.ts:1-48; backend/src/core/storage/adapters/filesystem.adapter.ts:1-86) and there is no user-
context module in backend/src/core.
- docs/DATABASE-SCHEMA.md:109-111 asserts station caching happens in Redis, but Station data is persisted in Postgres tables such as station_cache
(backend/src/features/stations/data/stations.repository.ts:11-115), and docs/DATABASE-SCHEMA.md:155-157 mentions “RESTRICT on delete” even though
migrations use ON DELETE CASCADE (backend/src/features/fuel-logs/migrations/001_create_fuel_logs_table.sql:18-21; backend/src/features/maintenance/
migrations/002_recreate_maintenance_tables.sql:21-43).
Questions
- Do we want to add the missing make test / make test-frontend automation (so the documented workflow survives), or should the documentation be rewritten to
direct people to the existing docker compose exec ... npm test commands?
- For the vehicles dropdown flow, should the docs be updated to call out the current TODOs, or is finishing the proxy implementation (and aligning the
frontend/client responses) a higher priority?
Suggested next steps: decide on the build/test command strategy, refresh the security/platform documentation to match the Auth0 setup and real cache
behaviour, and schedule a pass over the feature READMEs (vehicles, fuel logs, documents) so they match the implemented API contracts.

View File

@@ -3,10 +3,10 @@
This document explains the endtoend Vehicles API architecture after the platform service rebuild, how the MotoVaultPro app consumes it, how migrations/seeding work, and how to operate the stack in productiononly development. This document explains the endtoend Vehicles API architecture after the platform service rebuild, how the MotoVaultPro app consumes it, how migrations/seeding work, and how to operate the stack in productiononly development.
## Overview ## Overview
- Architecture: MotoVaultPro Application Service (Fastify + TS) consumes the MVP Platform service (FastAPI) with shared Postgres and Redis. - Architecture: MotoVaultPro backend (Fastify + TypeScript) includes an integrated platform module that shares Postgres and Redis with the rest of the stack.
- Goal: Predictable year→make→model→trim→engine cascades, productiononly workflow, AIfriendly code layout and docs. - Goal: Predictable year→make→model→trim→engine cascades, productiononly workflow, AIfriendly code layout and docs.
## Platform Vehicles Service ## Platform Vehicles Module
### Database Schema (Postgres schema: `vehicles`) ### Database Schema (Postgres schema: `vehicles`)
- `make(id, name)` - `make(id, name)`
@@ -20,12 +20,12 @@ This document explains the endtoend Vehicles API architecture after the pl
Idempotent constraints/indexes added where applicable (e.g., unique lower(name), unique(model_id, year), guarded `CREATE INDEX IF NOT EXISTS`, guarded trigger). Idempotent constraints/indexes added where applicable (e.g., unique lower(name), unique(model_id, year), guarded `CREATE INDEX IF NOT EXISTS`, guarded trigger).
### API Endpoints (Bearer auth required) ### API Endpoints (Bearer auth required)
Prefix: `/api/v1/vehicles` Prefix: `/api/platform`
- `GET /years``[number]` distinct years (desc) - `GET /api/platform/years``[number]` distinct years (desc)
- `GET /makes?year={year}``{ makes: { id, name }[] }` - `GET /api/platform/makes?year={year}``{ makes: { id, name }[] }`
- `GET /models?year={year}&make_id={make_id}``{ models: { id, name }[] }` - `GET /api/platform/models?year={year}&make_id={make_id}``{ models: { id, name }[] }`
- `GET /trims?year={year}&make_id={make_id}&model_id={model_id}``{ trims: { id, name }[] }` - `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}``{ trims: { id, name }[] }`
- `GET /engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}``{ engines: { id, name }[] }` - `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}``{ engines: { id, name }[] }`
Notes: Notes:
- `make_id` is maintained for a consistent query chain, but engines are enforced by `(year, model_id, trim_id)`. - `make_id` is maintained for a consistent query chain, but engines are enforced by `(year, model_id, trim_id)`.
@@ -41,14 +41,13 @@ Notes:
- **Configurable**: Set via `CACHE_TTL` environment variable in seconds - **Configurable**: Set via `CACHE_TTL` environment variable in seconds
### Seeds & Specific Examples ### Seeds & Specific Examples
Seed files under `mvp-platform-services/vehicles/sql/schema/`: Legacy FastAPI SQL seed scripts covered:
- `001_schema.sql` base tables - Base schema (`001_schema.sql`)
- `002_constraints_indexes.sql` constraints/indexes - Constraints/indexes (`002_constraints_indexes.sql`)
- `003_seed_minimal.sql` minimal Honda/Toyota scaffolding - Minimal Honda/Toyota scaffolding (`003_seed_minimal.sql`)
- `004_seed_filtered_makes.sql` Chevrolet/GMC examples - Chevrolet/GMC examples (`004_seed_filtered_makes.sql`)
- `005_seed_specific_vehicles.sql` requested examples: - Targeted sample vehicles (`005_seed_specific_vehicles.sql`)
- 2023 GMC Sierra 1500 AT4x → Engine L87 (6.2L V8) Contact the data team for access to these archival scripts if reseeding is required.
- 2017 Chevrolet Corvette Z06 Convertible → Engine LT4 (6.2L V8 SC)
Reapply seeds on an existing volume: Reapply seeds on an existing volume:
- `docker compose exec -T mvp-postgres psql -U mvp_user -d mvp_db -f /docker-entrypoint-initdb.d/005_seed_specific_vehicles.sql` - `docker compose exec -T mvp-postgres psql -U mvp_user -d mvp_db -f /docker-entrypoint-initdb.d/005_seed_specific_vehicles.sql`
@@ -133,22 +132,18 @@ VIN/License rule
### Rebuild a single service ### Rebuild a single service
- Frontend: `docker compose up -d --build frontend` - Frontend: `docker compose up -d --build frontend`
- Backend: `docker compose up -d --build backend` - Backend (includes platform module): `docker compose up -d --build backend`
- Platform API: `docker compose up -d --build mvp-platform`
### Logs & Health ### Logs & Health
- Backend: `/health` shows status/feature list - Backend: `/health` shows status/feature list, including platform readiness
- Platform: `/health` shows database/cache status - Logs: `make logs-backend`, `make logs-frontend`
- Logs:
- `make logs-backend`, `make logs-frontend`
- `docker compose logs -f mvp-platform`
### Common Reset Sequences ### Common Reset Sequences
- Platform seed reapply (nondestructive): apply `005_seed_specific_vehicles.sql` and flush Redis cache. - Platform seed reapply (nondestructive): apply `005_seed_specific_vehicles.sql` and flush Redis cache.
- Platform reset (WARNING - DESTRUCTIVE to shared resources): - Platform reset (WARNING - DESTRUCTIVE to shared resources):
- `docker compose rm -sf mvp-postgres mvp-redis` - `docker compose rm -sf mvp-postgres mvp-redis`
- `docker volume rm motovaultpro_postgres_data motovaultpro_redis_data` - `docker volume rm motovaultpro_postgres_data motovaultpro_redis_data`
- `docker compose up -d mvp-postgres mvp-redis mvp-platform` - `docker compose up -d mvp-postgres mvp-redis mvp-backend`
- Note: This will destroy ALL application data, not just platform data, as database and cache are shared - Note: This will destroy ALL application data, not just platform data, as database and cache are shared
## Security Summary ## Security Summary
@@ -169,9 +164,9 @@ VIN/License rule
- Ensure Postgres is up; the runner now waits/retries, but confirm logs. - Ensure Postgres is up; the runner now waits/retries, but confirm logs.
## Notable Files ## Notable Files
- Platform schema & seeds: `mvp-platform-services/vehicles/sql/schema/001..005` - Platform schema & seeds: maintained by database admins (legacy FastAPI scripts available on request)
- Platform API code: `mvp-platform-services/vehicles/api/*` - Platform API integration: `backend/src/features/platform/api/*`
- Backend dropdown proxy: `backend/src/features/vehicles/api/*` - Backend dropdown proxy: `backend/src/features/vehicles/api/*`
- Backend platform client: `backend/src/features/vehicles/external/platform-vehicles/*` - Backend platform module: `backend/src/features/platform/*`
- Backend migrations runner: `backend/src/_system/migrations/run-all.ts` - Backend migrations runner: `backend/src/_system/migrations/run-all.ts`
- Frontend vehicles UI: `frontend/src/features/vehicles/*` - Frontend vehicles UI: `frontend/src/features/vehicles/*`

View File

@@ -1,21 +0,0 @@
# Main nginx proxy for subdomain routing
nginx-proxy:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx-proxy/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- mvp-platform-landing
- admin-frontend
restart: unless-stopped
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -1,70 +0,0 @@
events {
worker_connections 1024;
}
http {
# Catch-all HTTP -> HTTPS redirect (handles localhost and unknown hosts)
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
# Main domain - Landing page
server {
listen 80;
server_name motovaultpro.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name motovaultpro.com;
ssl_certificate /etc/nginx/certs/motovaultpro.com.crt;
ssl_certificate_key /etc/nginx/certs/motovaultpro.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://mvp-platform-landing:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Admin subdomain - Admin tenant
server {
listen 80;
server_name admin.motovaultpro.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name admin.motovaultpro.com;
ssl_certificate /etc/nginx/certs/motovaultpro.com.crt;
ssl_certificate_key /etc/nginx/certs/motovaultpro.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://admin-frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
proxy_pass http://admin-backend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -192,19 +192,9 @@ EOF
echo "localdev123" > "$SECRETS_DIR/postgres-password.txt" echo "localdev123" > "$SECRETS_DIR/postgres-password.txt"
echo "minioadmin" > "$SECRETS_DIR/minio-access-key.txt" echo "minioadmin" > "$SECRETS_DIR/minio-access-key.txt"
echo "minioadmin123" > "$SECRETS_DIR/minio-secret-key.txt" echo "minioadmin123" > "$SECRETS_DIR/minio-secret-key.txt"
echo "mvp-platform-tenants-secret-key" > "$SECRETS_DIR/platform-tenants-api-key.txt"
echo "admin-backend-service-token" > "$SECRETS_DIR/service-auth-token.txt" echo "admin-backend-service-token" > "$SECRETS_DIR/service-auth-token.txt"
echo "your-auth0-client-secret" > "$SECRETS_DIR/auth0-client-secret.txt" echo "your-auth0-client-secret" > "$SECRETS_DIR/auth0-client-secret.txt"
echo "your-google-maps-api-key" > "$SECRETS_DIR/google-maps-api-key.txt" echo "your-google-maps-api-key" > "$SECRETS_DIR/google-maps-api-key.txt"
EOF
;;
"platform")
cat >> "$template_file" << 'EOF'
# Platform secrets
echo "platform123" > "$SECRETS_DIR/platform-db-password.txt"
echo "platform123" > "$SECRETS_DIR/vehicles-db-password.txt"
echo "mvp-platform-tenants-secret-key" > "$SECRETS_DIR/tenants-api-key.txt"
echo "admin-backend-service-token,mvp-platform-vehicles-service-token" > "$SECRETS_DIR/allowed-service-tokens.txt"
EOF EOF
;; ;;
esac esac