Pre-web changes
This commit is contained in:
@@ -7,6 +7,7 @@ import pool from '../../core/config/database';
|
||||
import { cacheService } from '../../core/config/redis';
|
||||
import { VINDecodeService } from './domain/vin-decode.service';
|
||||
import { PlatformCacheService } from './domain/platform-cache.service';
|
||||
import { VehicleDataService } from './domain/vehicle-data.service';
|
||||
|
||||
export { platformRoutes } from './api/platform.routes';
|
||||
export { PlatformController } from './api/platform.controller';
|
||||
@@ -18,6 +19,7 @@ export * from './models/responses';
|
||||
|
||||
// Singleton VIN decode service for use by other features
|
||||
let vinDecodeServiceInstance: VINDecodeService | null = null;
|
||||
let vehicleDataServiceInstance: VehicleDataService | null = null;
|
||||
|
||||
export function getVINDecodeService(): VINDecodeService {
|
||||
if (!vinDecodeServiceInstance) {
|
||||
@@ -27,6 +29,14 @@ export function getVINDecodeService(): VINDecodeService {
|
||||
return vinDecodeServiceInstance;
|
||||
}
|
||||
|
||||
export function getVehicleDataService(): VehicleDataService {
|
||||
if (!vehicleDataServiceInstance) {
|
||||
const platformCache = new PlatformCacheService(cacheService);
|
||||
vehicleDataServiceInstance = new VehicleDataService(platformCache);
|
||||
}
|
||||
return vehicleDataServiceInstance;
|
||||
}
|
||||
|
||||
// Helper to get pool for VIN decode service
|
||||
export function getPool(): Pool {
|
||||
return pool;
|
||||
|
||||
@@ -203,7 +203,7 @@ make setup
|
||||
make logs
|
||||
|
||||
# Verify
|
||||
curl http://localhost:3001/health
|
||||
curl https://motovaultpro.com/api/health
|
||||
```
|
||||
|
||||
### Verification
|
||||
@@ -215,7 +215,7 @@ docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key
|
||||
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
|
||||
|
||||
# Test API endpoint
|
||||
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/stations/saved
|
||||
curl -H "Authorization: Bearer $TOKEN" https://motovaultpro.com/api/stations/saved
|
||||
```
|
||||
|
||||
## Next Steps (Phases 6-11)
|
||||
|
||||
@@ -19,7 +19,7 @@ Authorization: Bearer {jwt_token}
|
||||
|
||||
## Base URL
|
||||
|
||||
**Development**: `http://localhost:3001/api/stations`
|
||||
**Development**: `https://motovaultpro.com/api/stations`
|
||||
|
||||
**Production**: `https://motovaultpro.com/api/stations`
|
||||
|
||||
@@ -574,7 +574,7 @@ TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
```bash
|
||||
# 1. Search for stations
|
||||
curl -X POST http://localhost:3001/api/stations/search \
|
||||
curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
@@ -585,7 +585,7 @@ curl -X POST http://localhost:3001/api/stations/search \
|
||||
|
||||
# 2. Save a station from search results
|
||||
PLACE_ID="ChIJN1t_tDeuEmsRUsoyG83frY4"
|
||||
curl -X POST http://localhost:3001/api/stations/save \
|
||||
curl -X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
@@ -595,11 +595,11 @@ curl -X POST http://localhost:3001/api/stations/save \
|
||||
}" | jq
|
||||
|
||||
# 3. Get all saved stations
|
||||
curl -X GET http://localhost:3001/api/stations/saved \
|
||||
curl -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $TOKEN" | jq
|
||||
|
||||
# 4. Update saved station
|
||||
curl -X PATCH http://localhost:3001/api/stations/saved/$PLACE_ID \
|
||||
curl -X PATCH https://motovaultpro.com/api/stations/saved/$PLACE_ID \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
@@ -607,7 +607,7 @@ curl -X PATCH http://localhost:3001/api/stations/saved/$PLACE_ID \
|
||||
}' | jq
|
||||
|
||||
# 5. Delete saved station
|
||||
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \
|
||||
curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
@@ -644,7 +644,7 @@ Google Maps API key is loaded from `/run/secrets/google-maps-api-key` at contain
|
||||
|
||||
API endpoints are configured to accept requests from:
|
||||
- https://motovaultpro.com (production)
|
||||
- http://localhost:3000 (development)
|
||||
- https://motovaultpro.com (development)
|
||||
|
||||
## Versioning
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ The Gas Stations feature requires two Google Maps APIs:
|
||||
- Add allowed domains:
|
||||
```
|
||||
https://motovaultpro.com/*
|
||||
http://localhost:3000/* # Development only
|
||||
https://motovaultpro.com/* # Development only
|
||||
```
|
||||
3. Click **Save**
|
||||
|
||||
@@ -273,7 +273,7 @@ Cost:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
secrets/app/google-maps-api-key.txt → Development key (restricted to localhost)
|
||||
secrets/app/google-maps-api-key.txt → Development key (restricted to motovaultpro.com)
|
||||
|
||||
# Production
|
||||
Kubernetes secret → Production key (restricted to production IPs)
|
||||
@@ -421,7 +421,7 @@ After setup, verify everything works:
|
||||
- [ ] Billing alerts configured
|
||||
- [ ] Key tested in development:
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/stations/search \
|
||||
curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}'
|
||||
|
||||
@@ -275,7 +275,7 @@ docker compose ps
|
||||
# All should show "Up" status
|
||||
|
||||
# Check backend health
|
||||
curl -s http://localhost:3001/health | jq
|
||||
curl -s https://motovaultpro.com/api/health | jq
|
||||
# Should return: {"status":"ok","features":["stations",...]}
|
||||
|
||||
# Check frontend loads
|
||||
@@ -292,11 +292,11 @@ Run comprehensive health checks to verify deployment:
|
||||
**Backend Health**:
|
||||
```bash
|
||||
# Overall health check
|
||||
curl http://localhost:3001/health
|
||||
curl https://motovaultpro.com/api/health
|
||||
|
||||
# Stations feature health (implicit in API availability)
|
||||
curl -H "Authorization: Bearer $TEST_TOKEN" \
|
||||
http://localhost:3001/api/stations/saved
|
||||
https://motovaultpro.com/api/stations/saved
|
||||
```
|
||||
|
||||
**Frontend Health**:
|
||||
@@ -343,7 +343,7 @@ export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
**Test Search Endpoint**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/stations/search \
|
||||
curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
@@ -379,14 +379,14 @@ curl -X POST http://localhost:3001/api/stations/search \
|
||||
**Test Save Endpoint**:
|
||||
```bash
|
||||
# First search to populate cache
|
||||
PLACE_ID=$(curl -X POST http://localhost:3001/api/stations/search \
|
||||
PLACE_ID=$(curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}' | \
|
||||
jq -r '.stations[0].placeId')
|
||||
|
||||
# Then save station
|
||||
curl -X POST http://localhost:3001/api/stations/save \
|
||||
curl -X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
@@ -398,13 +398,13 @@ curl -X POST http://localhost:3001/api/stations/save \
|
||||
|
||||
**Test Get Saved Endpoint**:
|
||||
```bash
|
||||
curl -X GET http://localhost:3001/api/stations/saved \
|
||||
curl -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $TOKEN" | jq
|
||||
```
|
||||
|
||||
**Test Delete Endpoint**:
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \
|
||||
curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
# Should return 204 No Content
|
||||
```
|
||||
@@ -437,20 +437,20 @@ Verify users can only access their own data:
|
||||
```bash
|
||||
# User 1 saves a station
|
||||
USER1_TOKEN="..."
|
||||
PLACE_ID=$(curl -X POST http://localhost:3001/api/stations/search \
|
||||
PLACE_ID=$(curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $USER1_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}' | \
|
||||
jq -r '.stations[0].placeId')
|
||||
|
||||
curl -X POST http://localhost:3001/api/stations/save \
|
||||
curl -X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $USER1_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"placeId\": \"$PLACE_ID\"}"
|
||||
|
||||
# User 2 should NOT see User 1's saved station
|
||||
USER2_TOKEN="..."
|
||||
curl -X GET http://localhost:3001/api/stations/saved \
|
||||
curl -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $USER2_TOKEN" | jq
|
||||
# Should return: [] (empty array)
|
||||
```
|
||||
@@ -461,13 +461,13 @@ Test response times meet requirements:
|
||||
|
||||
```bash
|
||||
# Search endpoint (should be < 1500ms)
|
||||
time curl -X POST http://localhost:3001/api/stations/search \
|
||||
time curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}'
|
||||
|
||||
# Saved stations endpoint (should be < 100ms)
|
||||
time curl -X GET http://localhost:3001/api/stations/saved \
|
||||
time curl -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
@@ -532,7 +532,7 @@ docker compose exec postgres psql -U postgres -d motovaultpro < backup_YYYYMMDD_
|
||||
docker compose up -d
|
||||
|
||||
# Verify rollback successful
|
||||
curl http://localhost:3001/health
|
||||
curl https://motovaultpro.com/api/health
|
||||
```
|
||||
|
||||
### Partial Rollback (Disable Feature)
|
||||
@@ -547,7 +547,7 @@ If only stations feature needs to be disabled:
|
||||
docker compose up -d --build mvp-backend
|
||||
|
||||
# Verify other features still work
|
||||
curl http://localhost:3001/health
|
||||
curl https://motovaultpro.com/api/health
|
||||
```
|
||||
|
||||
### Database Rollback Only
|
||||
@@ -655,7 +655,7 @@ docker compose logs mvp-backend | grep -i migration
|
||||
|
||||
- [ ] Check container health: `docker compose ps`
|
||||
- [ ] Review error logs: `docker compose logs | grep -i error`
|
||||
- [ ] Verify API responding: `curl http://localhost:3001/health`
|
||||
- [ ] Verify API responding: `curl https://motovaultpro.com/api/health`
|
||||
|
||||
### Weekly Checks
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Run this single command for immediate status:
|
||||
|
||||
```bash
|
||||
# All-in-one health check
|
||||
curl -s http://localhost:3001/health | jq
|
||||
curl -s https://motovaultpro.com/api/health | jq
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
@@ -39,7 +39,7 @@ export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
#### Test Search Endpoint
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/stations/search \
|
||||
curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
@@ -61,14 +61,14 @@ curl -X POST http://localhost:3001/api/stations/search \
|
||||
|
||||
```bash
|
||||
# First search to get a place ID
|
||||
PLACE_ID=$(curl -s -X POST http://localhost:3001/api/stations/search \
|
||||
PLACE_ID=$(curl -s -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}' \
|
||||
| jq -r '.stations[0].placeId')
|
||||
|
||||
# Then save station
|
||||
curl -X POST http://localhost:3001/api/stations/save \
|
||||
curl -X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"placeId\": \"$PLACE_ID\", \"nickname\": \"Health Check Station\"}" \
|
||||
@@ -84,7 +84,7 @@ curl -X POST http://localhost:3001/api/stations/save \
|
||||
#### Test Get Saved Endpoint
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3001/api/stations/saved \
|
||||
curl -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n" \
|
||||
| jq
|
||||
@@ -98,7 +98,7 @@ curl -X GET http://localhost:3001/api/stations/saved \
|
||||
#### Test Delete Endpoint
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \
|
||||
curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n"
|
||||
```
|
||||
@@ -316,13 +316,13 @@ Verify users can only access their own data:
|
||||
USER1_TOKEN="..."
|
||||
|
||||
# User 1 saves a station
|
||||
PLACE_ID=$(curl -s -X POST http://localhost:3001/api/stations/search \
|
||||
PLACE_ID=$(curl -s -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $USER1_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}' \
|
||||
| jq -r '.stations[0].placeId')
|
||||
|
||||
curl -s -X POST http://localhost:3001/api/stations/save \
|
||||
curl -s -X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $USER1_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"placeId\": \"$PLACE_ID\"}" > /dev/null
|
||||
@@ -331,7 +331,7 @@ curl -s -X POST http://localhost:3001/api/stations/save \
|
||||
USER2_TOKEN="..."
|
||||
|
||||
# User 2 tries to access User 1's saved stations
|
||||
curl -s -X GET http://localhost:3001/api/stations/saved \
|
||||
curl -s -X GET https://motovaultpro.com/api/stations/saved \
|
||||
-H "Authorization: Bearer $USER2_TOKEN" \
|
||||
| jq '. | length'
|
||||
```
|
||||
@@ -346,7 +346,7 @@ curl -s -X GET http://localhost:3001/api/stations/saved \
|
||||
# Run 10 searches and measure average time
|
||||
for i in {1..10}; do
|
||||
curl -s -o /dev/null -w "%{time_total}\n" \
|
||||
-X POST http://localhost:3001/api/stations/search \
|
||||
-X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}'
|
||||
@@ -360,7 +360,7 @@ done | awk '{sum+=$1} END {print "Average:", sum/NR, "seconds"}'
|
||||
```bash
|
||||
# Measure save operation time
|
||||
time curl -s -o /dev/null \
|
||||
-X POST http://localhost:3001/api/stations/save \
|
||||
-X POST https://motovaultpro.com/api/stations/save \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"placeId\": \"$PLACE_ID\"}"
|
||||
@@ -379,7 +379,7 @@ sudo apt-get install apache2-utils
|
||||
# Run load test (100 requests, 10 concurrent)
|
||||
ab -n 100 -c 10 -T 'application/json' -H "Authorization: Bearer $TOKEN" \
|
||||
-p search_payload.json \
|
||||
http://localhost:3001/api/stations/search
|
||||
https://motovaultpro.com/api/stations/search
|
||||
```
|
||||
|
||||
**Expected**:
|
||||
@@ -415,7 +415,7 @@ if [ -z "$TOKEN" ]; then
|
||||
fi
|
||||
|
||||
echo "1. Backend Health Check..."
|
||||
if curl -s http://localhost:3001/health | jq -e '.status == "ok"' > /dev/null; then
|
||||
if curl -s https://motovaultpro.com/api/health | jq -e '.status == "ok"' > /dev/null; then
|
||||
echo "${GREEN}✓ Backend healthy${NC}"
|
||||
else
|
||||
echo "${RED}✗ Backend unhealthy${NC}"
|
||||
@@ -444,7 +444,7 @@ fi
|
||||
|
||||
echo ""
|
||||
echo "4. Search API Check..."
|
||||
if curl -s -X POST http://localhost:3001/api/stations/search \
|
||||
if curl -s -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"latitude": 37.7749, "longitude": -122.4194}' \
|
||||
@@ -492,7 +492,7 @@ JWT_TOKEN="your_token_here" ./health-check.sh
|
||||
- [ ] Backend container running: `docker compose ps mvp-backend`
|
||||
- [ ] Frontend container running: `docker compose ps mvp-frontend`
|
||||
- [ ] No errors in logs: `docker compose logs --tail=100 | grep -i error`
|
||||
- [ ] Health endpoint responding: `curl http://localhost:3001/health`
|
||||
- [ ] Health endpoint responding: `curl https://motovaultpro.com/api/health`
|
||||
|
||||
### Weekly Monitoring
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ Complete production readiness checklist for the Gas Stations feature. This docum
|
||||
|
||||
#### Immediate Validation (within 5 minutes)
|
||||
|
||||
- [ ] Health endpoint responding: `curl http://localhost:3001/health`
|
||||
- [ ] Health endpoint responding: `curl https://motovaultpro.com/api/health`
|
||||
- [ ] Backend logs show no errors: `docker compose logs mvp-backend | grep -i error`
|
||||
- [ ] Frontend logs show no errors: `docker compose logs mvp-frontend | grep -i error`
|
||||
- [ ] Database tables exist: `\dt station*`
|
||||
|
||||
@@ -140,7 +140,7 @@ docker compose logs mvp-frontend | grep -i config
|
||||
|
||||
**Check config.js is served**:
|
||||
```bash
|
||||
curl -s http://localhost:3000/config.js
|
||||
curl -s https://motovaultpro.com/config.js
|
||||
```
|
||||
|
||||
**Expected**: JavaScript file with `window.CONFIG = {...}`
|
||||
@@ -214,7 +214,7 @@ Results: 20 stations
|
||||
export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
# Search for stations
|
||||
curl -X POST http://localhost:3001/api/stations/search \
|
||||
curl -X POST https://motovaultpro.com/api/stations/search \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
@@ -400,7 +400,7 @@ docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
|
||||
|
||||
2. **Check config.js content**:
|
||||
```bash
|
||||
curl http://localhost:3000/config.js
|
||||
curl https://motovaultpro.com/config.js
|
||||
```
|
||||
|
||||
3. **Check index.html loads config.js**:
|
||||
|
||||
@@ -13,16 +13,17 @@ Primary entity for vehicle management consuming MVP Platform Vehicles Service. H
|
||||
- `DELETE /api/vehicles/:id` - Soft delete vehicle
|
||||
|
||||
### Hierarchical Vehicle Dropdowns
|
||||
**Status**: Dropdown methods are TODO stubs in vehicles service. Frontend directly consumes platform module endpoints.
|
||||
**Status**: Vehicles service now proxies the platform vehicle catalog to provide fully dynamic dropdowns. Each selection step filters the next list, ensuring only valid combinations are shown.
|
||||
|
||||
Frontend consumes (via `/platform` module, not vehicles feature):
|
||||
- `GET /api/platform/years` - Get all years
|
||||
- `GET /api/platform/makes?year={year}` - Get makes for year
|
||||
- `GET /api/platform/models?year={year}&make_id={make_id}` - Get models for make/year
|
||||
- `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}` - Get trims
|
||||
- `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}` - Get engines
|
||||
- `GET /api/platform/transmissions?year={year}&make_id={make_id}&model_id={model_id}` - Get transmissions
|
||||
- `GET /api/platform/vehicle?vin={vin}` - Decode VIN
|
||||
Sequence:
|
||||
1. `GET /api/vehicles/dropdown/years` → `[number]` (latest to oldest).
|
||||
2. `GET /api/vehicles/dropdown/makes?year={year}` → `{ id, name }[]` (only makes produced in the selected year).
|
||||
3. `GET /api/vehicles/dropdown/models?year={year}&make_id={id}` → `{ id, name }[]` (models offered for that year/make).
|
||||
4. `GET /api/vehicles/dropdown/trims?year={year}&make_id={id}&model_id={id}` → `{ id, name }[]` (valid trims for the chosen combination).
|
||||
5. `GET /api/vehicles/dropdown/engines?year={year}&make_id={id}&model_id={id}&trim_id={id}` → `{ id, name }[]` (engines tied to the trim).
|
||||
6. `GET /api/vehicles/dropdown/transmissions?year={year}&make_id={id}&model_id={id}` → `{ id, name }[]` (static options: Automatic, Manual).
|
||||
|
||||
All dropdown endpoints call `Platform VehicleDataService` behind the scenes, reuse Redis caching, and return normalized `{ id, name }` payloads ready for the frontend.
|
||||
|
||||
## Authentication
|
||||
- All vehicles endpoints (including dropdowns) require a valid JWT (Auth0).
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { VehiclesRepository } from '../data/vehicles.repository';
|
||||
import { getVINDecodeService, getPool } from '../../platform';
|
||||
import { getVINDecodeService, getVehicleDataService, getPool } from '../../platform';
|
||||
import {
|
||||
Vehicle,
|
||||
CreateVehicleRequest,
|
||||
@@ -162,52 +162,52 @@ export class VehiclesService {
|
||||
await cacheService.del(cacheKey);
|
||||
}
|
||||
|
||||
async getDropdownMakes(_year: number): Promise<{ id: number; name: string }[]> {
|
||||
// TODO: Implement using platform VehicleDataService
|
||||
// For now, return empty array to allow migration to complete
|
||||
logger.warn('Dropdown makes not yet implemented via platform feature');
|
||||
return [];
|
||||
async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
|
||||
const vehicleDataService = getVehicleDataService();
|
||||
const pool = getPool();
|
||||
|
||||
logger.info('Fetching dropdown makes via platform module', { year });
|
||||
return vehicleDataService.getMakes(pool, year);
|
||||
}
|
||||
|
||||
async getDropdownModels(_year: number, _makeId: number): Promise<{ id: number; name: string }[]> {
|
||||
// TODO: Implement using platform VehicleDataService
|
||||
logger.warn('Dropdown models not yet implemented via platform feature');
|
||||
return [];
|
||||
async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
|
||||
const vehicleDataService = getVehicleDataService();
|
||||
const pool = getPool();
|
||||
|
||||
logger.info('Fetching dropdown models via platform module', { year, makeId });
|
||||
return vehicleDataService.getModels(pool, year, makeId);
|
||||
}
|
||||
|
||||
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
|
||||
// TODO: Implement using platform VehicleDataService
|
||||
logger.warn('Dropdown transmissions not yet implemented via platform feature');
|
||||
return [];
|
||||
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> {
|
||||
logger.info('Providing dropdown transmissions from static list');
|
||||
return [
|
||||
{ id: 1, name: 'Automatic' },
|
||||
{ id: 2, name: 'Manual' }
|
||||
];
|
||||
}
|
||||
|
||||
async getDropdownEngines(_year: number, _makeId: number, _modelId: number, _trimId: number): Promise<{ name: string }[]> {
|
||||
// TODO: Implement using platform VehicleDataService
|
||||
logger.warn('Dropdown engines not yet implemented via platform feature');
|
||||
return [];
|
||||
async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]> {
|
||||
const vehicleDataService = getVehicleDataService();
|
||||
const pool = getPool();
|
||||
|
||||
logger.info('Fetching dropdown engines via platform module', { year, makeId, modelId, trimId });
|
||||
return vehicleDataService.getEngines(pool, year, modelId, trimId);
|
||||
}
|
||||
|
||||
async getDropdownTrims(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
|
||||
// TODO: Implement using platform VehicleDataService
|
||||
logger.warn('Dropdown trims not yet implemented via platform feature');
|
||||
return [];
|
||||
async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]> {
|
||||
const vehicleDataService = getVehicleDataService();
|
||||
const pool = getPool();
|
||||
|
||||
logger.info('Fetching dropdown trims via platform module', { year, makeId, modelId });
|
||||
return vehicleDataService.getTrims(pool, year, modelId);
|
||||
}
|
||||
|
||||
async getDropdownYears(): Promise<number[]> {
|
||||
try {
|
||||
logger.info('Getting dropdown years');
|
||||
// Fallback: generate recent years
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years: number[] = [];
|
||||
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
|
||||
return years;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get dropdown years', { error });
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years: number[] = [];
|
||||
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
|
||||
return years;
|
||||
}
|
||||
const vehicleDataService = getVehicleDataService();
|
||||
const pool = getPool();
|
||||
|
||||
logger.info('Fetching dropdown years via platform module');
|
||||
return vehicleDataService.getYears(pool);
|
||||
}
|
||||
|
||||
async decodeVIN(vin: string): Promise<{
|
||||
|
||||
@@ -12,20 +12,42 @@ import * as platformModule from '../../../platform';
|
||||
jest.mock('../../data/vehicles.repository');
|
||||
jest.mock('../../../../core/config/redis');
|
||||
jest.mock('../../../platform', () => ({
|
||||
getVINDecodeService: jest.fn()
|
||||
getVINDecodeService: jest.fn(),
|
||||
getVehicleDataService: jest.fn(),
|
||||
getPool: jest.fn()
|
||||
}));
|
||||
|
||||
const mockRepository = jest.mocked(VehiclesRepository);
|
||||
const mockCacheService = jest.mocked(cacheService);
|
||||
const mockGetVINDecodeService = jest.mocked(platformModule.getVINDecodeService);
|
||||
const mockGetVehicleDataService = jest.mocked(platformModule.getVehicleDataService);
|
||||
const mockGetPool = jest.mocked(platformModule.getPool);
|
||||
|
||||
describe('VehiclesService', () => {
|
||||
let service: VehiclesService;
|
||||
let repositoryInstance: jest.Mocked<VehiclesRepository>;
|
||||
let vehicleDataServiceMock: {
|
||||
getYears: jest.Mock;
|
||||
getMakes: jest.Mock;
|
||||
getModels: jest.Mock;
|
||||
getTrims: jest.Mock;
|
||||
getEngines: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
vehicleDataServiceMock = {
|
||||
getYears: jest.fn(),
|
||||
getMakes: jest.fn(),
|
||||
getModels: jest.fn(),
|
||||
getTrims: jest.fn(),
|
||||
getEngines: jest.fn(),
|
||||
};
|
||||
|
||||
mockGetVehicleDataService.mockReturnValue(vehicleDataServiceMock as any);
|
||||
mockGetPool.mockReturnValue('mock-pool' as any);
|
||||
|
||||
repositoryInstance = {
|
||||
create: jest.fn(),
|
||||
findByUserId: jest.fn(),
|
||||
@@ -39,6 +61,63 @@ describe('VehiclesService', () => {
|
||||
service = new VehiclesService(repositoryInstance);
|
||||
});
|
||||
|
||||
describe('dropdown data integration', () => {
|
||||
it('retrieves years from platform service', async () => {
|
||||
vehicleDataServiceMock.getYears.mockResolvedValue([2024, 2023]);
|
||||
|
||||
const result = await service.getDropdownYears();
|
||||
|
||||
expect(mockGetVehicleDataService).toHaveBeenCalled();
|
||||
expect(vehicleDataServiceMock.getYears).toHaveBeenCalledWith('mock-pool');
|
||||
expect(result).toEqual([2024, 2023]);
|
||||
});
|
||||
|
||||
it('retrieves makes scoped to year', async () => {
|
||||
vehicleDataServiceMock.getMakes.mockResolvedValue([{ id: 10, name: 'Honda' }]);
|
||||
|
||||
const result = await service.getDropdownMakes(2024);
|
||||
|
||||
expect(vehicleDataServiceMock.getMakes).toHaveBeenCalledWith('mock-pool', 2024);
|
||||
expect(result).toEqual([{ id: 10, name: 'Honda' }]);
|
||||
});
|
||||
|
||||
it('retrieves models scoped to year and make', async () => {
|
||||
vehicleDataServiceMock.getModels.mockResolvedValue([{ id: 20, name: 'Civic' }]);
|
||||
|
||||
const result = await service.getDropdownModels(2024, 10);
|
||||
|
||||
expect(vehicleDataServiceMock.getModels).toHaveBeenCalledWith('mock-pool', 2024, 10);
|
||||
expect(result).toEqual([{ id: 20, name: 'Civic' }]);
|
||||
});
|
||||
|
||||
it('retrieves trims scoped to year, make, and model', async () => {
|
||||
vehicleDataServiceMock.getTrims.mockResolvedValue([{ id: 30, name: 'Sport' }]);
|
||||
|
||||
const result = await service.getDropdownTrims(2024, 10, 20);
|
||||
|
||||
expect(vehicleDataServiceMock.getTrims).toHaveBeenCalledWith('mock-pool', 2024, 20);
|
||||
expect(result).toEqual([{ id: 30, name: 'Sport' }]);
|
||||
});
|
||||
|
||||
it('retrieves engines scoped to selection', async () => {
|
||||
vehicleDataServiceMock.getEngines.mockResolvedValue([{ id: 40, name: '2.0L Turbo' }]);
|
||||
|
||||
const result = await service.getDropdownEngines(2024, 10, 20, 30);
|
||||
|
||||
expect(vehicleDataServiceMock.getEngines).toHaveBeenCalledWith('mock-pool', 2024, 20, 30);
|
||||
expect(result).toEqual([{ id: 40, name: '2.0L Turbo' }]);
|
||||
});
|
||||
|
||||
it('returns static transmission options', async () => {
|
||||
const result = await service.getDropdownTransmissions(2024, 10, 20);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, name: 'Automatic' },
|
||||
{ id: 2, name: 'Manual' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('createVehicle', () => {
|
||||
const mockVehicleData = {
|
||||
vin: '1HGBH41JXMN109186',
|
||||
@@ -94,7 +173,7 @@ describe('VehiclesService', () => {
|
||||
const result = await service.createVehicle(mockVehicleData, 'user-123');
|
||||
|
||||
expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186');
|
||||
expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('1HGBH41JXMN109186');
|
||||
expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('mock-pool', '1HGBH41JXMN109186');
|
||||
expect(repositoryInstance.create).toHaveBeenCalledWith({
|
||||
...mockVehicleData,
|
||||
userId: 'user-123',
|
||||
@@ -319,4 +398,4 @@ describe('VehiclesService', () => {
|
||||
await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user