Docs Cleanup
This commit is contained in:
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)}
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -41,12 +41,6 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
|
||||
+------------------+ +--------------------+
|
||||
(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
|
||||
```
|
||||
|
||||
### Backend to Platform Flow
|
||||
### Backend Platform Flow
|
||||
```
|
||||
1. Backend Feature → HTTP Request → Platform Service
|
||||
- URL: http://mvp-platform:8000
|
||||
- Headers: Authorization: Bearer <API_KEY>
|
||||
2. Platform → Query PostgreSQL (vehicles schema)
|
||||
3. Platform → Check/Update Redis Cache
|
||||
4. Platform → Response → Backend
|
||||
1. Backend Feature → Calls platform domain services (TypeScript in-process)
|
||||
2. Platform module → Query PostgreSQL (vehicles schema)
|
||||
3. Platform module → Check/Update Redis cache
|
||||
4. Platform response → Returned to caller (vehicles, frontend proxies, etc.)
|
||||
```
|
||||
|
||||
### Data Access Flow
|
||||
@@ -196,53 +188,51 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
|
||||
- Session storage (future)
|
||||
- Rate limiting (future)
|
||||
|
||||
### Platform (Vehicle Data Service)
|
||||
- **Technology**: Python 3.11 + FastAPI
|
||||
- **Build**: Custom Dockerfile from `mvp-platform-services/vehicles/`
|
||||
- **Port**: 8000 (internal)
|
||||
- **Networks**: backend, database
|
||||
- **Dependencies**: PostgreSQL, Redis
|
||||
- **Health Check**: `wget http://localhost:8000/health` (30s interval)
|
||||
### Platform Module (Vehicle Data)
|
||||
- **Technology**: TypeScript feature capsule inside `backend/src/features/platform/`
|
||||
- **Runtime**: Shares the `mvp-backend` Fastify container
|
||||
- **Dependencies**: PostgreSQL, Redis (accessed via backend connection pool)
|
||||
- **Deployment**: Bundled with backend build; no separate container or port
|
||||
- **Health**: Covered by backend `/health` endpoint and feature-specific logs
|
||||
- **Configuration**:
|
||||
- `/app/config/production.yml` - Service config
|
||||
- `/app/config/shared.yml` - Shared config
|
||||
- `backend/src/features/platform/domain/*.ts` - Business logic
|
||||
- `backend/src/features/platform/data/*.ts` - Database + vPIC integration
|
||||
- **Secrets**:
|
||||
- `postgres-password` - Shared database access
|
||||
- Reuses backend secrets (PostgreSQL, Auth0, etc.)
|
||||
- **Purpose**:
|
||||
- Vehicle make/model/trim/engine data
|
||||
- VIN decoding (planned)
|
||||
- Standardized vehicle information
|
||||
|
||||
## Platform Service Integration
|
||||
## Platform Module Integration
|
||||
|
||||
### Current Architecture
|
||||
The Platform service is a **separate Python container** that provides vehicle data capabilities to the backend. This separation exists for:
|
||||
- **Technology Independence**: Python ecosystem for data processing
|
||||
- **Specialized Caching**: Year-based hierarchical caching strategy
|
||||
- **Resource Isolation**: Independent scaling and monitoring
|
||||
Platform capabilities now run **in-process** within the backend as a feature capsule. This delivers:
|
||||
- **Shared Language & Tooling**: TypeScript across the stack
|
||||
- **Centralized Caching**: Reuses backend Redis helpers for year-based lookups
|
||||
- **Simplified Operations**: No extra container to build, scale, or monitor
|
||||
|
||||
### Shared Infrastructure
|
||||
- **Database**: Both Backend and Platform use `mvp-postgres`
|
||||
- **Cache**: Both services share `mvp-redis`
|
||||
- **Network**: Connected via `backend` and `database` networks
|
||||
- **Secrets**: Share database credentials
|
||||
- **Database**: Uses the backend connection pool against `mvp-postgres`
|
||||
- **Cache**: Shares the backend Redis client (`mvp-redis`)
|
||||
- **Runtime**: Packaged with the `mvp-backend` container and Fastify plugins
|
||||
- **Secrets**: Relies on the same secret files as the backend (PostgreSQL, Auth0, etc.)
|
||||
|
||||
### Communication Pattern
|
||||
```javascript
|
||||
// Backend calls Platform via internal HTTP
|
||||
const response = await fetch('http://mvp-platform:8000/api/v1/vehicles/makes?year=2024', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${platformApiKey}`
|
||||
}
|
||||
```typescript
|
||||
// Vehicles feature pulling platform data via domain services
|
||||
const platformService = new PlatformVehicleDataService({
|
||||
db: postgresPool,
|
||||
cache: redisClient
|
||||
});
|
||||
|
||||
const makes = await platformService.getMakes({ year: 2024 });
|
||||
```
|
||||
|
||||
### Future Evolution
|
||||
**Planned**: Absorb Platform service into Backend as a feature capsule
|
||||
- Timeline: Post-MVP phase
|
||||
- Approach: Rewrite Python service in Node.js
|
||||
- Location: `backend/src/features/vehicle-platform/`
|
||||
- Benefits: Simplified deployment, shared codebase, reduced containers
|
||||
### Ongoing Work
|
||||
- Continue migrating remaining FastAPI utilities into TypeScript where needed
|
||||
- Expand `/api/platform/*` endpoints as new lookup types are added
|
||||
- Monitor backend logs for `platform` namespace entries to verify health
|
||||
|
||||
## Feature Capsule Architecture
|
||||
|
||||
@@ -255,7 +245,8 @@ backend/src/features/
|
||||
├── fuel-logs/ # Fuel tracking
|
||||
├── maintenance/ # Service records
|
||||
├── stations/ # Gas station locations
|
||||
└── documents/ # Document storage
|
||||
├── documents/ # Document storage
|
||||
└── platform/ # Vehicle data + VIN decoding
|
||||
```
|
||||
|
||||
### Feature Structure
|
||||
@@ -372,7 +363,6 @@ The application uses Kubernetes-inspired configuration patterns:
|
||||
|
||||
**Secrets** (File-based):
|
||||
- `/run/secrets/postgres-password` - Database credentials
|
||||
- `/run/secrets/platform-vehicles-api-key` - Service auth
|
||||
- `/run/secrets/auth0-client-secret` - OAuth credentials
|
||||
- `/run/secrets/google-maps-api-key` - External API keys
|
||||
|
||||
@@ -384,14 +374,13 @@ CONFIG_PATH: /app/config/production.yml
|
||||
SECRETS_DIR: /run/secrets
|
||||
DATABASE_HOST: mvp-postgres
|
||||
REDIS_HOST: mvp-redis
|
||||
PLATFORM_VEHICLES_API_URL: http://mvp-platform:8000
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Container Commands (via Makefile)
|
||||
```bash
|
||||
make start # Start all 6 containers
|
||||
make start # Start all 5 containers
|
||||
make stop # Stop all containers
|
||||
make restart # Restart all containers
|
||||
make rebuild # Rebuild and restart containers
|
||||
@@ -407,12 +396,10 @@ docker ps
|
||||
|
||||
# View specific service logs
|
||||
docker logs mvp-backend -f
|
||||
docker logs mvp-platform -f
|
||||
docker logs mvp-postgres -f
|
||||
|
||||
# Test health endpoints
|
||||
curl http://localhost:3001/health # Backend
|
||||
curl http://localhost:8000/health # Platform
|
||||
curl http://localhost:3001/health # Backend (includes platform module)
|
||||
```
|
||||
|
||||
### Database Access
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# MVP Platform Service
|
||||
# MVP Platform Module
|
||||
|
||||
## 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
|
||||
|
||||
The platform service is integrated into the main application stack:
|
||||
- **Service Container**: mvp-platform
|
||||
The platform module runs as part of the backend service:
|
||||
- **Runtime**: `mvp-backend` container (Fastify API)
|
||||
- **Shared Database**: Uses mvp-postgres
|
||||
- **Shared Cache**: Uses mvp-redis
|
||||
|
||||
@@ -37,18 +37,17 @@ The platform provides vehicle data capabilities including:
|
||||
|
||||
**Start All Services**:
|
||||
```bash
|
||||
make start # Starts all 6 containers
|
||||
make start # Starts the five-container stack
|
||||
```
|
||||
|
||||
**Service Logs**:
|
||||
**Backend Logs (includes platform module)**:
|
||||
```bash
|
||||
make logs # All service logs
|
||||
docker logs mvp-platform
|
||||
make logs-backend
|
||||
```
|
||||
|
||||
**Service Shell Access**:
|
||||
**Backend Shell (platform code lives here)**:
|
||||
```bash
|
||||
docker exec -it mvp-platform sh
|
||||
make shell-backend
|
||||
```
|
||||
|
||||
### Database Management
|
||||
@@ -66,10 +65,10 @@ make db-shell-app
|
||||
## Deployment Strategy
|
||||
|
||||
### Integrated Deployment
|
||||
The platform service deploys with the main application:
|
||||
The platform module deploys with the main application:
|
||||
- Same deployment pipeline
|
||||
- Shares database and cache
|
||||
- Deployed as part of 6-container stack
|
||||
- Deployed as part of the five-container stack
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
@@ -85,7 +84,7 @@ Application features access platform data through shared database:
|
||||
|
||||
**Service Discovery Problems**:
|
||||
- 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**:
|
||||
- Verify mvp-postgres is healthy
|
||||
@@ -93,19 +92,19 @@ Application features access platform data through shared database:
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Verify Platform Service**:
|
||||
**Verify Backend Service (contains platform module)**:
|
||||
```bash
|
||||
docker ps | grep mvp-platform
|
||||
docker compose ps mvp-backend
|
||||
```
|
||||
|
||||
### Logs and Debugging
|
||||
|
||||
**Service Logs**:
|
||||
```bash
|
||||
docker logs mvp-platform --tail=100 -f
|
||||
docker compose logs -f mvp-backend
|
||||
```
|
||||
|
||||
**Database Logs**:
|
||||
```bash
|
||||
docker logs mvp-postgres --tail=100 -f
|
||||
docker compose logs -f mvp-postgres
|
||||
```
|
||||
|
||||
@@ -14,11 +14,11 @@ MotoVaultPro uses a 4-agent team for optimal development velocity and quality en
|
||||
- Platform service integration
|
||||
- Backend tests and migrations
|
||||
|
||||
**Platform Service Agent** - Independent microservices
|
||||
- Building new platform services in `mvp-platform-services/{service}/`
|
||||
- FastAPI microservice development
|
||||
- ETL pipelines and service databases
|
||||
- Platform service tests and deployment
|
||||
**Platform Feature Agent** - Integrated platform module
|
||||
- Enhancing `backend/src/features/platform/`
|
||||
- Vehicle lookup + VIN decoding logic
|
||||
- Redis/SQL performance tuning for platform endpoints
|
||||
- Platform module tests and documentation
|
||||
|
||||
**Mobile-First Frontend Agent** - Responsive UI/UX
|
||||
- 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."
|
||||
Agent: Mobile-First Frontend Agent
|
||||
|
||||
# Platform microservice
|
||||
Task: "Create {service} platform microservice with FastAPI.
|
||||
Implement API, database, and health checks with tests."
|
||||
Agent: Platform Service Agent
|
||||
# Platform module work
|
||||
Task: "Enhance platform feature capsule (backend/src/features/platform).
|
||||
Implement API/domain/data changes with accompanying tests."
|
||||
Agent: Platform Feature Agent
|
||||
|
||||
# Quality validation
|
||||
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/`
|
||||
- After changes: `make rebuild` then check logs
|
||||
|
||||
### Platform Service Changes (Python)
|
||||
**Agent**: Platform Service Agent
|
||||
- Service: `mvp-platform-services/{service}/`
|
||||
- API: `mvp-platform-services/{service}/api/`
|
||||
- After changes: `make rebuild` then check service health
|
||||
### Platform Module Changes (TypeScript)
|
||||
**Agent**: Platform Feature Agent
|
||||
- Feature capsule: `backend/src/features/platform/`
|
||||
- API routes: `backend/src/features/platform/api/`
|
||||
- Domain/data: `backend/src/features/platform/domain/` and `data/`
|
||||
- After changes: `make rebuild` then verify platform endpoints via backend logs/tests
|
||||
|
||||
### Database Changes
|
||||
- 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
|
||||
5. Validate keyboard navigation on desktop
|
||||
|
||||
### Add Platform Service Integration
|
||||
**Agents**: Platform Service Agent + Feature Capsule Agent
|
||||
1. Platform Service Agent: Implement service endpoint
|
||||
2. Feature Capsule Agent: Create client in `external/platform-{service}/`
|
||||
3. Feature Capsule Agent: Add circuit breaker and caching
|
||||
4. Test integration with both agents
|
||||
### Add Platform Integration
|
||||
**Agents**: Platform Feature Agent + Feature Capsule Agent
|
||||
1. Platform Feature Agent: Implement/update platform endpoint logic
|
||||
2. Feature Capsule Agent: Update consuming feature client (e.g. `external/platform-vehicles/`)
|
||||
3. Feature Capsule Agent: Adjust caching/circuit breaker strategies as needed
|
||||
4. Joint testing: run targeted unit/integration suites
|
||||
5. Quality Enforcer Agent: Validate end-to-end
|
||||
|
||||
### Run Quality Checks
|
||||
@@ -188,9 +189,9 @@ Code is complete when:
|
||||
|
||||
## Architecture Quick Reference
|
||||
|
||||
### Hybrid Platform
|
||||
- **Platform Microservices**: Independent services in `mvp-platform-services/`
|
||||
- **Application Features**: Modular monolith in `backend/src/features/`
|
||||
### Application Stack
|
||||
- **Backend Feature Capsules**: Modular monolith in `backend/src/features/`
|
||||
- **Platform Module**: Vehicle data + VIN decoding in `backend/src/features/platform/`
|
||||
- **Frontend**: React SPA in `frontend/src/`
|
||||
|
||||
### Feature Capsule Pattern
|
||||
@@ -206,29 +207,21 @@ backend/src/features/{feature}/
|
||||
└── 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
|
||||
|
||||
- **Auth**: Frontend uses Auth0, backend validates JWTs
|
||||
- **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
|
||||
- **Testing**: Jest (backend/frontend), pytest (platform services)
|
||||
- **Testing**: Jest (backend + frontend)
|
||||
- **Docker-First**: All development in containers (production-only)
|
||||
|
||||
## Agent Coordination Rules
|
||||
|
||||
### Clear Ownership
|
||||
- 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
|
||||
- Quality Enforcer Agent: Testing and validation only
|
||||
|
||||
|
||||
@@ -22,5 +22,58 @@ Project documentation hub for the 5-container single-tenant architecture with in
|
||||
## Notes
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
## 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); there’s 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.
|
||||
@@ -3,10 +3,10 @@
|
||||
This document explains the end‑to‑end 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 production‑only development.
|
||||
|
||||
## 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, production‑only workflow, AI‑friendly code layout and docs.
|
||||
|
||||
## Platform Vehicles Service
|
||||
## Platform Vehicles Module
|
||||
|
||||
### Database Schema (Postgres schema: `vehicles`)
|
||||
- `make(id, name)`
|
||||
@@ -20,12 +20,12 @@ This document explains the end‑to‑end 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).
|
||||
|
||||
### API Endpoints (Bearer auth required)
|
||||
Prefix: `/api/v1/vehicles`
|
||||
- `GET /years` → `[number]` distinct years (desc)
|
||||
- `GET /makes?year={year}` → `{ makes: { id, name }[] }`
|
||||
- `GET /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 /engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}` → `{ engines: { id, name }[] }`
|
||||
Prefix: `/api/platform`
|
||||
- `GET /api/platform/years` → `[number]` distinct years (desc)
|
||||
- `GET /api/platform/makes?year={year}` → `{ makes: { id, name }[] }`
|
||||
- `GET /api/platform/models?year={year}&make_id={make_id}` → `{ models: { id, name }[] }`
|
||||
- `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}` → `{ trims: { id, name }[] }`
|
||||
- `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}` → `{ engines: { id, name }[] }`
|
||||
|
||||
Notes:
|
||||
- `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
|
||||
|
||||
### Seeds & Specific Examples
|
||||
Seed files under `mvp-platform-services/vehicles/sql/schema/`:
|
||||
- `001_schema.sql` – base tables
|
||||
- `002_constraints_indexes.sql` – constraints/indexes
|
||||
- `003_seed_minimal.sql` – minimal Honda/Toyota scaffolding
|
||||
- `004_seed_filtered_makes.sql` – Chevrolet/GMC examples
|
||||
- `005_seed_specific_vehicles.sql` – requested examples:
|
||||
- 2023 GMC Sierra 1500 AT4x → Engine L87 (6.2L V8)
|
||||
- 2017 Chevrolet Corvette Z06 Convertible → Engine LT4 (6.2L V8 SC)
|
||||
Legacy FastAPI SQL seed scripts covered:
|
||||
- Base schema (`001_schema.sql`)
|
||||
- Constraints/indexes (`002_constraints_indexes.sql`)
|
||||
- Minimal Honda/Toyota scaffolding (`003_seed_minimal.sql`)
|
||||
- Chevrolet/GMC examples (`004_seed_filtered_makes.sql`)
|
||||
- Targeted sample vehicles (`005_seed_specific_vehicles.sql`)
|
||||
Contact the data team for access to these archival scripts if reseeding is required.
|
||||
|
||||
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`
|
||||
@@ -133,22 +132,18 @@ VIN/License rule
|
||||
|
||||
### Rebuild a single service
|
||||
- Frontend: `docker compose up -d --build frontend`
|
||||
- Backend: `docker compose up -d --build backend`
|
||||
- Platform API: `docker compose up -d --build mvp-platform`
|
||||
- Backend (includes platform module): `docker compose up -d --build backend`
|
||||
|
||||
### Logs & Health
|
||||
- Backend: `/health` – shows status/feature list
|
||||
- Platform: `/health` – shows database/cache status
|
||||
- Logs:
|
||||
- `make logs-backend`, `make logs-frontend`
|
||||
- `docker compose logs -f mvp-platform`
|
||||
- Backend: `/health` – shows status/feature list, including platform readiness
|
||||
- Logs: `make logs-backend`, `make logs-frontend`
|
||||
|
||||
### Common Reset Sequences
|
||||
- Platform seed reapply (non‑destructive): apply `005_seed_specific_vehicles.sql` and flush Redis cache.
|
||||
- Platform reset (WARNING - DESTRUCTIVE to shared resources):
|
||||
- `docker compose rm -sf mvp-postgres mvp-redis`
|
||||
- `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
|
||||
|
||||
## Security Summary
|
||||
@@ -169,9 +164,9 @@ VIN/License rule
|
||||
- Ensure Postgres is up; the runner now waits/retries, but confirm logs.
|
||||
|
||||
## Notable Files
|
||||
- Platform schema & seeds: `mvp-platform-services/vehicles/sql/schema/001..005`
|
||||
- Platform API code: `mvp-platform-services/vehicles/api/*`
|
||||
- Platform schema & seeds: maintained by database admins (legacy FastAPI scripts available on request)
|
||||
- Platform API integration: `backend/src/features/platform/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`
|
||||
- Frontend vehicles UI: `frontend/src/features/vehicles/*`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,19 +192,9 @@ EOF
|
||||
echo "localdev123" > "$SECRETS_DIR/postgres-password.txt"
|
||||
echo "minioadmin" > "$SECRETS_DIR/minio-access-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 "your-auth0-client-secret" > "$SECRETS_DIR/auth0-client-secret.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
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user