fix: Clean up docs
All checks were successful
Deploy to Staging / Build Images (push) Successful in 2m39s
Deploy to Staging / Deploy to Staging (push) Successful in 28s
Deploy to Staging / Verify Staging (push) Successful in 6s
Deploy to Staging / Notify Staging Ready (push) Successful in 5s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
All checks were successful
Deploy to Staging / Build Images (push) Successful in 2m39s
Deploy to Staging / Deploy to Staging (push) Successful in 28s
Deploy to Staging / Verify Staging (push) Successful in 6s
Deploy to Staging / Notify Staging Ready (push) Successful in 5s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
This commit is contained in:
600
docs/ADMIN.md
600
docs/ADMIN.md
@@ -1,600 +0,0 @@
|
|||||||
# Admin Feature Documentation
|
|
||||||
|
|
||||||
Complete reference for the admin role management, authorization, and cross-tenant oversight capabilities in MotoVaultPro.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The admin feature provides role-based access control for system administrators to manage:
|
|
||||||
- Admin user accounts (create, revoke, reinstate)
|
|
||||||
- Vehicle catalog data (makes, models, years, trims, engines)
|
|
||||||
- Gas stations and user favorites
|
|
||||||
- Complete audit trail of all admin actions
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Backend Feature Capsule
|
|
||||||
|
|
||||||
Location: `backend/src/features/admin/`
|
|
||||||
|
|
||||||
Structure:
|
|
||||||
```
|
|
||||||
admin/
|
|
||||||
├── api/
|
|
||||||
│ ├── admin.controller.ts - HTTP handlers for admin management
|
|
||||||
│ ├── admin.routes.ts - Route registration
|
|
||||||
│ ├── admin.validation.ts - Input validation schemas
|
|
||||||
│ ├── catalog.controller.ts - Vehicle catalog handlers
|
|
||||||
│ └── stations.controller.ts - Station oversight handlers
|
|
||||||
├── domain/
|
|
||||||
│ ├── admin.types.ts - TypeScript type definitions
|
|
||||||
│ ├── admin.service.ts - Admin user management logic
|
|
||||||
│ ├── vehicle-catalog.service.ts - Catalog CRUD logic
|
|
||||||
│ └── station-oversight.service.ts - Station management logic
|
|
||||||
├── data/
|
|
||||||
│ └── admin.repository.ts - Database access layer
|
|
||||||
├── migrations/
|
|
||||||
│ ├── 001_create_admin_users.sql - Admin tables and seed
|
|
||||||
│ └── 002_create_platform_change_log.sql - Catalog audit log
|
|
||||||
└── tests/
|
|
||||||
├── unit/ - Service and guard tests
|
|
||||||
├── integration/ - Full API endpoint tests
|
|
||||||
└── fixtures/ - Test data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Core Plugins
|
|
||||||
|
|
||||||
- **auth.plugin.ts**: Enhanced with `request.userContext` containing `userId`, `email`, `isAdmin`, `adminRecord`
|
|
||||||
- **admin-guard.plugin.ts**: `fastify.requireAdmin` preHandler that checks `admin_users` table and enforces 403 on non-admins
|
|
||||||
|
|
||||||
### Frontend Feature
|
|
||||||
|
|
||||||
Location: `frontend/src/features/admin/`
|
|
||||||
|
|
||||||
Structure:
|
|
||||||
```
|
|
||||||
admin/
|
|
||||||
├── types/admin.types.ts - TypeScript types (mirroring backend)
|
|
||||||
├── api/admin.api.ts - API client functions
|
|
||||||
├── hooks/
|
|
||||||
│ ├── useAdminAccess.ts - Verify admin status
|
|
||||||
│ ├── useAdmins.ts - Admin user management
|
|
||||||
│ ├── useCatalog.ts - Vehicle catalog
|
|
||||||
│ └── useStationOverview.ts - Station management
|
|
||||||
├── pages/
|
|
||||||
│ ├── AdminUsersPage.tsx - Desktop user management
|
|
||||||
│ ├── AdminCatalogPage.tsx - Desktop catalog management
|
|
||||||
│ └── AdminStationsPage.tsx - Desktop station management
|
|
||||||
├── mobile/
|
|
||||||
│ ├── AdminUsersMobileScreen.tsx - Mobile user management
|
|
||||||
│ ├── AdminCatalogMobileScreen.tsx - Mobile catalog management
|
|
||||||
│ └── AdminStationsMobileScreen.tsx - Mobile station management
|
|
||||||
└── __tests__/ - Component and hook tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### admin_users table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE admin_users (
|
|
||||||
auth0_sub VARCHAR(255) PRIMARY KEY,
|
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
role VARCHAR(50) NOT NULL DEFAULT 'admin',
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by VARCHAR(255) NOT NULL,
|
|
||||||
revoked_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Indexes:**
|
|
||||||
- `auth0_sub` (PRIMARY KEY) - OAuth ID from Auth0
|
|
||||||
- `email` - For admin lookups by email
|
|
||||||
- `created_at` - For audit trails
|
|
||||||
- `revoked_at` - For active admin queries
|
|
||||||
|
|
||||||
### admin_audit_logs table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE admin_audit_logs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
actor_admin_id VARCHAR(255) NOT NULL,
|
|
||||||
target_admin_id VARCHAR(255),
|
|
||||||
action VARCHAR(100) NOT NULL,
|
|
||||||
resource_type VARCHAR(100),
|
|
||||||
resource_id VARCHAR(255),
|
|
||||||
context JSONB,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Actions logged:**
|
|
||||||
- CREATE - New admin or resource created
|
|
||||||
- UPDATE - Resource updated
|
|
||||||
- DELETE - Resource deleted
|
|
||||||
- REVOKE - Admin access revoked
|
|
||||||
- REINSTATE - Admin access restored
|
|
||||||
- VIEW - Data accessed (for sensitive operations)
|
|
||||||
|
|
||||||
### platform_change_log table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE platform_change_log (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
change_type VARCHAR(50) NOT NULL,
|
|
||||||
resource_type VARCHAR(100) NOT NULL,
|
|
||||||
resource_id VARCHAR(255),
|
|
||||||
old_value JSONB,
|
|
||||||
new_value JSONB,
|
|
||||||
changed_by VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resource types:**
|
|
||||||
- makes, models, years, trims, engines
|
|
||||||
- stations
|
|
||||||
- users
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### Phase 2: Admin Management
|
|
||||||
|
|
||||||
#### List all admins
|
|
||||||
```
|
|
||||||
GET /api/admin/admins
|
|
||||||
Authorization: Bearer <JWT>
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"total": 2,
|
|
||||||
"admins": [
|
|
||||||
{
|
|
||||||
"auth0Sub": "auth0|admin1",
|
|
||||||
"email": "admin@motovaultpro.com",
|
|
||||||
"role": "admin",
|
|
||||||
"createdAt": "2024-01-01T00:00:00Z",
|
|
||||||
"createdBy": "system",
|
|
||||||
"revokedAt": null,
|
|
||||||
"updatedAt": "2024-01-01T00:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create admin
|
|
||||||
```
|
|
||||||
POST /api/admin/admins
|
|
||||||
Authorization: Bearer <JWT>
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
Request:
|
|
||||||
{
|
|
||||||
"email": "newadmin@example.com",
|
|
||||||
"role": "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
Response (201):
|
|
||||||
{
|
|
||||||
"auth0Sub": "auth0|newadmin",
|
|
||||||
"email": "newadmin@example.com",
|
|
||||||
"role": "admin",
|
|
||||||
"createdAt": "2024-01-15T10:30:00Z",
|
|
||||||
"createdBy": "auth0|existing",
|
|
||||||
"revokedAt": null,
|
|
||||||
"updatedAt": "2024-01-15T10:30:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
Audit log entry:
|
|
||||||
{
|
|
||||||
"actor_admin_id": "auth0|existing",
|
|
||||||
"target_admin_id": "auth0|newadmin",
|
|
||||||
"action": "CREATE",
|
|
||||||
"resource_type": "admin_user",
|
|
||||||
"resource_id": "newadmin@example.com",
|
|
||||||
"context": { "email": "newadmin@example.com", "role": "admin" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Revoke admin
|
|
||||||
```
|
|
||||||
PATCH /api/admin/admins/:auth0Sub/revoke
|
|
||||||
Authorization: Bearer <JWT>
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"auth0Sub": "auth0|toadmin",
|
|
||||||
"email": "admin@motovaultpro.com",
|
|
||||||
"role": "admin",
|
|
||||||
"createdAt": "2024-01-01T00:00:00Z",
|
|
||||||
"createdBy": "system",
|
|
||||||
"revokedAt": "2024-01-15T10:35:00Z",
|
|
||||||
"updatedAt": "2024-01-15T10:35:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
Errors:
|
|
||||||
- 400 Bad Request - Last active admin (cannot revoke)
|
|
||||||
- 403 Forbidden - Not an admin
|
|
||||||
- 404 Not Found - Admin not found
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Reinstate admin
|
|
||||||
```
|
|
||||||
PATCH /api/admin/admins/:auth0Sub/reinstate
|
|
||||||
Authorization: Bearer <JWT>
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"auth0Sub": "auth0|toadmin",
|
|
||||||
"email": "admin@motovaultpro.com",
|
|
||||||
"role": "admin",
|
|
||||||
"createdAt": "2024-01-01T00:00:00Z",
|
|
||||||
"createdBy": "system",
|
|
||||||
"revokedAt": null,
|
|
||||||
"updatedAt": "2024-01-15T10:40:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Audit logs
|
|
||||||
```
|
|
||||||
GET /api/admin/audit-logs?limit=100&offset=0
|
|
||||||
Authorization: Bearer <JWT>
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"total": 150,
|
|
||||||
"logs": [
|
|
||||||
{
|
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"actor_admin_id": "auth0|admin1",
|
|
||||||
"target_admin_id": "auth0|admin2",
|
|
||||||
"action": "CREATE",
|
|
||||||
"resource_type": "admin_user",
|
|
||||||
"resource_id": "admin2@motovaultpro.com",
|
|
||||||
"context": { "email": "admin2@motovaultpro.com", "role": "admin" },
|
|
||||||
"created_at": "2024-01-15T10:30:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Catalog CRUD
|
|
||||||
|
|
||||||
All catalog endpoints follow RESTful patterns:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/admin/catalog/{resource} - List all
|
|
||||||
GET /api/admin/catalog/{parent}/{parentId}/{resource} - List by parent
|
|
||||||
POST /api/admin/catalog/{resource} - Create
|
|
||||||
PUT /api/admin/catalog/{resource}/:id - Update
|
|
||||||
DELETE /api/admin/catalog/{resource}/:id - Delete
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resources:** makes, models, years, trims, engines
|
|
||||||
|
|
||||||
**Example: Get all makes**
|
|
||||||
```
|
|
||||||
GET /api/admin/catalog/makes
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"total": 42,
|
|
||||||
"makes": [
|
|
||||||
{ "id": "1", "name": "Toyota", "createdAt": "...", "updatedAt": "..." },
|
|
||||||
{ "id": "2", "name": "Honda", "createdAt": "...", "updatedAt": "..." }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cache invalidation:** All mutations invalidate `platform:*` Redis keys
|
|
||||||
|
|
||||||
**Audit trail:** All mutations recorded in `platform_change_log` with old and new values
|
|
||||||
|
|
||||||
### Phase 4: Station Oversight
|
|
||||||
|
|
||||||
#### List all stations
|
|
||||||
```
|
|
||||||
GET /api/admin/stations?limit=100&offset=0&search=query
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"total": 1250,
|
|
||||||
"stations": [
|
|
||||||
{
|
|
||||||
"id": "station-1",
|
|
||||||
"placeId": "ChIJxxx",
|
|
||||||
"name": "Shell Station Downtown",
|
|
||||||
"address": "123 Main St",
|
|
||||||
"latitude": 40.7128,
|
|
||||||
"longitude": -74.0060,
|
|
||||||
"createdAt": "...",
|
|
||||||
"deletedAt": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create station
|
|
||||||
```
|
|
||||||
POST /api/admin/stations
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
Request:
|
|
||||||
{
|
|
||||||
"placeId": "ChIJxxx",
|
|
||||||
"name": "New Station",
|
|
||||||
"address": "456 Oak Ave",
|
|
||||||
"latitude": 40.7580,
|
|
||||||
"longitude": -73.9855
|
|
||||||
}
|
|
||||||
|
|
||||||
Response (201): Station object with all fields
|
|
||||||
|
|
||||||
Cache invalidation:
|
|
||||||
- mvp:stations:* - All station caches
|
|
||||||
- mvp:stations:search:* - Search result caches
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Delete station (soft or hard)
|
|
||||||
```
|
|
||||||
DELETE /api/admin/stations/:stationId?force=false
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Query parameters:
|
|
||||||
- force=false (default) - Soft delete (set deleted_at)
|
|
||||||
- force=true - Hard delete (permanent removal)
|
|
||||||
|
|
||||||
Response (204 No Content)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### User station management
|
|
||||||
```
|
|
||||||
GET /api/admin/users/:userId/stations
|
|
||||||
Guard: fastify.requireAdmin
|
|
||||||
|
|
||||||
Response (200):
|
|
||||||
{
|
|
||||||
"userId": "auth0|user123",
|
|
||||||
"stations": [...]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authorization Rules
|
|
||||||
|
|
||||||
### Admin Guard
|
|
||||||
|
|
||||||
The `fastify.requireAdmin` preHandler enforces:
|
|
||||||
|
|
||||||
1. **JWT validation** - User must be authenticated
|
|
||||||
2. **Admin check** - User must exist in `admin_users` table
|
|
||||||
3. **Active status** - User's `revoked_at` must be NULL
|
|
||||||
4. **Error response** - Returns 403 Forbidden with message "Admin access required"
|
|
||||||
|
|
||||||
### Last Admin Protection
|
|
||||||
|
|
||||||
The system maintains at least one active admin:
|
|
||||||
- Cannot revoke the last active admin (returns 400 Bad Request)
|
|
||||||
- Prevents system lockout
|
|
||||||
- Enforced in `AdminService.revokeAdmin()`
|
|
||||||
|
|
||||||
### Audit Trail
|
|
||||||
|
|
||||||
All admin actions logged:
|
|
||||||
- Actor admin ID (who performed action)
|
|
||||||
- Target admin ID (who was affected, if applicable)
|
|
||||||
- Action type (CREATE, UPDATE, DELETE, REVOKE, REINSTATE)
|
|
||||||
- Resource type and ID
|
|
||||||
- Context (relevant data, like old/new values)
|
|
||||||
- Timestamp
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
|
|
||||||
All inputs validated using Zod schemas:
|
|
||||||
- Email format and uniqueness
|
|
||||||
- Role enum validation
|
|
||||||
- Required field presence
|
|
||||||
- Type checking
|
|
||||||
|
|
||||||
### Parameterized Queries
|
|
||||||
|
|
||||||
All database operations use parameterized queries:
|
|
||||||
```typescript
|
|
||||||
// Good - Parameterized
|
|
||||||
const result = await pool.query(
|
|
||||||
'SELECT * FROM admin_users WHERE email = $1',
|
|
||||||
[email]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bad - SQL concatenation (never done)
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT * FROM admin_users WHERE email = '${email}'`
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transaction Support
|
|
||||||
|
|
||||||
Catalog mutations wrapped in transactions:
|
|
||||||
```sql
|
|
||||||
BEGIN;
|
|
||||||
-- INSERT/UPDATE/DELETE operations
|
|
||||||
COMMIT; -- or ROLLBACK on error
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cache Invalidation
|
|
||||||
|
|
||||||
Prevents stale data:
|
|
||||||
- All catalog mutations invalidate `platform:*` keys
|
|
||||||
- All station mutations invalidate `mvp:stations:*` keys
|
|
||||||
- User station mutations invalidate `mvp:stations:saved:{userId}`
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
1. **Database migrations** - Run all migrations before deploying
|
|
||||||
2. **Initial admin** - First admin seeded automatically in migration
|
|
||||||
3. **Auth0 configuration** - Admin user must exist in Auth0
|
|
||||||
|
|
||||||
### Deployment Steps
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build containers
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# 2. Run migrations (automatically on startup)
|
|
||||||
docker compose exec mvp-backend npm run migrate
|
|
||||||
|
|
||||||
# 3. Verify admin user created
|
|
||||||
docker compose exec mvp-backend npm run verify-admin
|
|
||||||
|
|
||||||
# 4. Check backend health
|
|
||||||
curl https://motovaultpro.com/api/health
|
|
||||||
|
|
||||||
# 5. Verify frontend build
|
|
||||||
curl https://motovaultpro.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback
|
|
||||||
|
|
||||||
If issues occur:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Revoke problematic admin
|
|
||||||
docker compose exec mvp-backend npm run admin:revoke admin@motovaultpro.com
|
|
||||||
|
|
||||||
# Reinstate previous admin
|
|
||||||
docker compose exec mvp-backend npm run admin:reinstate <auth0_sub>
|
|
||||||
|
|
||||||
# Downgrade admin feature (keep data)
|
|
||||||
docker compose down
|
|
||||||
git checkout previous-version
|
|
||||||
make rebuild
|
|
||||||
make start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Backend Unit Tests
|
|
||||||
|
|
||||||
Location: `backend/src/features/admin/tests/unit/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- features/admin/tests/unit
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Admin guard authorization logic
|
|
||||||
- Admin service business rules
|
|
||||||
- Repository error handling
|
|
||||||
- Last admin protection
|
|
||||||
|
|
||||||
### Backend Integration Tests
|
|
||||||
|
|
||||||
Location: `backend/src/features/admin/tests/integration/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- features/admin/tests/integration
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Full API endpoints
|
|
||||||
- Database persistence
|
|
||||||
- Audit logging
|
|
||||||
- Admin guard in request context
|
|
||||||
- CRUD operations
|
|
||||||
- Cache invalidation
|
|
||||||
- Permission enforcement
|
|
||||||
|
|
||||||
### Frontend Tests
|
|
||||||
|
|
||||||
Location: `frontend/src/features/admin/__tests__/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec mvp-frontend npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- useAdminAccess hook (loading, admin, non-admin, error states)
|
|
||||||
- Admin page rendering
|
|
||||||
- Admin route guards
|
|
||||||
- Navigation
|
|
||||||
|
|
||||||
### E2E Testing
|
|
||||||
|
|
||||||
1. **Desktop workflow**
|
|
||||||
- Navigate to `/garage/settings`
|
|
||||||
- Verify "Admin Console" card visible (if admin)
|
|
||||||
- Click "User Management"
|
|
||||||
- Verify admin list loads
|
|
||||||
- Try to create new admin (if permitted)
|
|
||||||
|
|
||||||
2. **Mobile workflow**
|
|
||||||
- Open app on mobile viewport (375px)
|
|
||||||
- Navigate to settings
|
|
||||||
- Verify admin section visible (if admin)
|
|
||||||
- Tap "Users" button
|
|
||||||
- Verify admin list loads
|
|
||||||
|
|
||||||
## Monitoring & Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Issue: "Admin access required" (403 Forbidden)**
|
|
||||||
- Verify user in `admin_users` table
|
|
||||||
- Check `revoked_at` is NULL
|
|
||||||
- Verify JWT token valid
|
|
||||||
- Check Auth0 configuration
|
|
||||||
|
|
||||||
**Issue: Stale catalog data**
|
|
||||||
- Verify Redis is running
|
|
||||||
- Check cache invalidation logs
|
|
||||||
- Manually flush: `redis-cli DEL 'mvp:platform:*'`
|
|
||||||
|
|
||||||
**Issue: Audit log not recording**
|
|
||||||
- Check `admin_audit_logs` table exists
|
|
||||||
- Verify migrations ran
|
|
||||||
- Check database connection
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
View admin-related logs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend logs
|
|
||||||
make logs | grep -i admin
|
|
||||||
|
|
||||||
# Check specific action
|
|
||||||
docker compose exec mvp-backend psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM admin_audit_logs WHERE action = 'CREATE' ORDER BY created_at DESC LIMIT 10;"
|
|
||||||
|
|
||||||
# Check revoked admins
|
|
||||||
docker compose exec mvp-backend psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT email, revoked_at FROM admin_users WHERE revoked_at IS NOT NULL;"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### Planned Enhancements
|
|
||||||
|
|
||||||
1. **Role-based permissions** - Extend from binary admin to granular roles (admin, catalog_editor, station_manager)
|
|
||||||
2. **2FA for admins** - Enhanced security with two-factor authentication
|
|
||||||
3. **Admin impersonation** - Test user issues as admin without changing password
|
|
||||||
4. **Bulk operations** - Import/export catalog data
|
|
||||||
5. **Advanced analytics** - Admin activity dashboards
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Backend feature: `backend/src/features/admin/README.md`
|
|
||||||
- Frontend feature: `frontend/src/features/admin/` (see individual files)
|
|
||||||
- Architecture: `docs/PLATFORM-SERVICES.md`
|
|
||||||
- Testing: `docs/TESTING.md`
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
### 1.1 High-Level Assessment
|
### 1.1 High-Level Assessment
|
||||||
|
|
||||||
**MotoVaultPro** is an Australian automotive vehicle management platform built on a modern 5-container Docker architecture. The application demonstrates solid architectural foundations with proper authentication, modular feature design, and production-ready deployment configuration.
|
**MotoVaultPro** is an automotive vehicle management platform built on a modern 5-container Docker architecture. The application demonstrates solid architectural foundations with proper authentication, modular feature design, and production-ready deployment configuration.
|
||||||
|
|
||||||
### 1.2 Key Findings Summary
|
### 1.2 Key Findings Summary
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
| `DATABASE-SCHEMA.md` | Database table definitions | Schema changes, data modeling |
|
| `DATABASE-SCHEMA.md` | Database table definitions | Schema changes, data modeling |
|
||||||
| `DATABASE-MIGRATION.md` | Migration procedures | Running or writing migrations |
|
| `DATABASE-MIGRATION.md` | Migration procedures | Running or writing migrations |
|
||||||
| `VEHICLES-API.md` | Vehicles API specification | Vehicle-related API work |
|
| `VEHICLES-API.md` | Vehicles API specification | Vehicle-related API work |
|
||||||
| `ADMIN.md` | Admin feature documentation | Admin role management, catalog CRUD |
|
|
||||||
| `TESTING.md` | Testing strategy and commands | Writing or running tests |
|
| `TESTING.md` | Testing strategy and commands | Writing or running tests |
|
||||||
| `E2E-TESTING.md` | End-to-end testing guide | Cypress E2E test setup |
|
| `E2E-TESTING.md` | End-to-end testing guide | Cypress E2E test setup |
|
||||||
| `SECURITY.md` | Security guidelines | Security considerations |
|
| `SECURITY.md` | Security guidelines | Security considerations |
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
| `AUDIT.md` | Audit documentation | Security audits, compliance |
|
| `AUDIT.md` | Audit documentation | Security audits, compliance |
|
||||||
| `MVP-COLOR-SCHEME.md` | Color scheme reference | UI styling decisions |
|
| `MVP-COLOR-SCHEME.md` | Color scheme reference | UI styling decisions |
|
||||||
| `UX-DEBUGGING.md` | UX debugging techniques | Debugging UI issues |
|
| `UX-DEBUGGING.md` | UX debugging techniques | Debugging UI issues |
|
||||||
| `PROMPTS.md` | AI prompt templates | AI-assisted development |
|
|
||||||
|
|
||||||
## Subdirectories
|
## Subdirectories
|
||||||
|
|
||||||
|
|||||||
111
docs/PROMPTS.md
111
docs/PROMPTS.md
@@ -1,111 +0,0 @@
|
|||||||
*** CRITICAL ***: Never read this file. Stop and move on.
|
|
||||||
|
|
||||||
|
|
||||||
*** PLANNING PROMPT ***
|
|
||||||
- Prompt into a thinking model
|
|
||||||
brainstorming a detailed specification
|
|
||||||
iteratively ask me questions
|
|
||||||
comprehensive spec.md - containing requirements, architecture decisions, data models, and even a testing strategy. This spec forms the foundation for development.
|
|
||||||
|
|
||||||
- Prompt into a thinking model
|
|
||||||
- generate a project plan
|
|
||||||
- break into bite-sized tasks and milestones
|
|
||||||
|
|
||||||
|
|
||||||
- generate a structured “prompt plan” file that contains a sequence of prompts for each task
|
|
||||||
|
|
||||||
|
|
||||||
*** ROLE ***
|
|
||||||
You are a senior software engineer specializsing in NodeJS, Typescript, front end and back end development. You will be delegating tasks to the platform-agent, feature-agent, first-frontend-agent and quality-agent when appropriate.
|
|
||||||
|
|
||||||
*** ACTION ***
|
|
||||||
- Make no assumptions.
|
|
||||||
- Ask clarifying questions.
|
|
||||||
- Ultrathink
|
|
||||||
|
|
||||||
*** CONTEXT ***
|
|
||||||
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
|
|
||||||
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
|
|
||||||
|
|
||||||
*** CHANGES TO IMPLEMENT ***
|
|
||||||
- Research this code base and ask iterative questions to compile a complete plan.
|
|
||||||
- We will pair troubleshoot this. Tell me what logs and things to run and I will
|
|
||||||
- There is current a Dark / Light theme option for this application
|
|
||||||
- There is logic somewhere in the code that detects the operating systems' theme and uses that. Remove this.
|
|
||||||
- Default to the
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*** ROLE ***
|
|
||||||
- You are a senior DevOps SRE with expert knowledge of Python, Ansible, GitHub and GitLab pipelines.
|
|
||||||
|
|
||||||
*** ACTION ***
|
|
||||||
- Make no assumptions.
|
|
||||||
- Ask clarifying questions.
|
|
||||||
- Ultrathink
|
|
||||||
- Debug why staging and production websites dont' match even though the docker image ID's match
|
|
||||||
- Analysis needs to be done on the CI/CD pipeline
|
|
||||||
|
|
||||||
*** CONTEXT ***
|
|
||||||
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
|
|
||||||
- The staging site runs on staging.motovaultpro.com and production runs on motovaultpro.com
|
|
||||||
- These sites are local so use an MCP that will work with local sites to gather a snapshot.
|
|
||||||
- Example: Staging has the correct title in About Us "Built by enthusiasts. Made for your collection."
|
|
||||||
- Exaxmple: Production has the old title in About us "Overall, our goal is to meet each individual's needs with quality, passion, and professionalism."
|
|
||||||
|
|
||||||
*** ACTION - CHANGES TO IMPLEMENT ***
|
|
||||||
- Research this code base and ask iterative questions to compile a complete plan.
|
|
||||||
- We will pair plan this. Ask me for options for various levels of redundancy and automation
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*** ROLE ***
|
|
||||||
- You are a senior DBA with expert knowledge in Postgres SQL.
|
|
||||||
|
|
||||||
*** ACTION ***
|
|
||||||
- Make no assumptions.
|
|
||||||
- Ask clarifying questions.
|
|
||||||
- Ultrathink
|
|
||||||
- You will be implementing an ETL process that takes a export of the NHTSA vPIC database in Postgres and transforming it for use in this application.
|
|
||||||
|
|
||||||
*** CONTEXT ***
|
|
||||||
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
|
|
||||||
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
|
|
||||||
- There is an existing database import process in this directory. This process works and should not be changed.
|
|
||||||
- The source database from the NHTSA vPIC dataset is located in the @vpic-source directory
|
|
||||||
- Deep research needs to be conducted on how to execute this ETL process.
|
|
||||||
- The source database is designed for VIN decoding only.
|
|
||||||
- Example VIN: 2025 Honda Civic Hybrid - 2HGFE4F88SH315466
|
|
||||||
- Example VIN: 2023 GMC Sierra 1500 AT4x - 3GTUUFEL6PG140748
|
|
||||||
- Example VIN: 2017 Chevrolet Corvette Z06 - 1G1YU3D64H5602799
|
|
||||||
|
|
||||||
*** CHANGES TO IMPLEMENT ***
|
|
||||||
- Research this code base and ask iterative questions to compile a complete plan.
|
|
||||||
- generate a project plan
|
|
||||||
- break into bite-sized tasks and milestones
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*** ROLE ***
|
|
||||||
- You are a expert frontend developer specializing in advanced techniques using Tailwind and React frameworks.
|
|
||||||
|
|
||||||
*** ACTION ***
|
|
||||||
- Make no assumptions.
|
|
||||||
- Ask clarifying questions.
|
|
||||||
- Ultrathink
|
|
||||||
- You will be making changes to email templates of this application.
|
|
||||||
|
|
||||||
*** CONTEXT ***
|
|
||||||
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
|
|
||||||
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
|
|
||||||
- Start your research at this route https://motovaultpro.com/garage/settings/admin/email-templates
|
|
||||||
- The email templates are currently plain text.
|
|
||||||
- The templates need to be improved with colors and the company logo
|
|
||||||
- The company log should be base64 encoded in the email so end users don't need to download anything.
|
|
||||||
- The theme should match the website light theme
|
|
||||||
- A screenshot showing the colors is attached
|
|
||||||
|
|
||||||
*** CHANGES TO IMPLEMENT ***
|
|
||||||
- Research this code base and ask iterative questions to compile a complete plan.
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
# Bulk Catalog Delete Endpoint Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Generic bulk delete endpoint for catalog entities (makes, models, years, trims, engines) in the admin panel.
|
|
||||||
|
|
||||||
## Endpoint
|
|
||||||
```
|
|
||||||
DELETE /api/admin/catalog/{entity}/bulk-delete
|
|
||||||
```
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
- `entity`: Entity type - one of: `makes`, `models`, `years`, `trims`, `engines`
|
|
||||||
|
|
||||||
## Request Body
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ids": [1, 2, 3, 4, 5]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation Rules
|
|
||||||
- IDs must be an array of positive integers
|
|
||||||
- At least 1 ID required
|
|
||||||
- Maximum 100 IDs per batch
|
|
||||||
- All IDs must be valid integers (not strings or floats)
|
|
||||||
|
|
||||||
## Response Codes
|
|
||||||
- `204 No Content`: All deletions succeeded (no response body)
|
|
||||||
- `207 Multi-Status`: Some deletions failed (includes response body with details)
|
|
||||||
- `400 Bad Request`: Invalid entity type or invalid request body
|
|
||||||
- `401 Unauthorized`: Missing or invalid authentication
|
|
||||||
- `500 Internal Server Error`: Unexpected server error
|
|
||||||
|
|
||||||
## Response Body (207 Multi-Status only)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deleted": [1, 3, 5],
|
|
||||||
"failed": [
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"error": "Make 2 not found"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"error": "Cannot delete make with existing models"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cascade Behavior
|
|
||||||
The endpoint uses existing single-delete methods which have the following behavior:
|
|
||||||
|
|
||||||
### Makes
|
|
||||||
- **Blocks deletion** if models exist under the make
|
|
||||||
- Error: "Cannot delete make with existing models"
|
|
||||||
- **Solution**: Delete all dependent models first
|
|
||||||
|
|
||||||
### Models
|
|
||||||
- **Blocks deletion** if years exist under the model
|
|
||||||
- Error: "Cannot delete model with existing years"
|
|
||||||
- **Solution**: Delete all dependent years first
|
|
||||||
|
|
||||||
### Years
|
|
||||||
- **Blocks deletion** if trims exist under the year
|
|
||||||
- Error: "Cannot delete year with existing trims"
|
|
||||||
- **Solution**: Delete all dependent trims first
|
|
||||||
|
|
||||||
### Trims
|
|
||||||
- **Blocks deletion** if engines exist under the trim
|
|
||||||
- Error: "Cannot delete trim with existing engines"
|
|
||||||
- **Solution**: Delete all dependent engines first
|
|
||||||
|
|
||||||
### Engines
|
|
||||||
- **No cascade restrictions** (leaf entity in hierarchy)
|
|
||||||
|
|
||||||
## Deletion Order for Hierarchy
|
|
||||||
To delete an entire make and all its dependencies:
|
|
||||||
1. Delete engines first
|
|
||||||
2. Delete trims
|
|
||||||
3. Delete years
|
|
||||||
4. Delete models
|
|
||||||
5. Delete make last
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Example 1: Delete Multiple Engines (Success)
|
|
||||||
```bash
|
|
||||||
DELETE /api/admin/catalog/engines/bulk-delete
|
|
||||||
{
|
|
||||||
"ids": [101, 102, 103]
|
|
||||||
}
|
|
||||||
|
|
||||||
Response: 204 No Content
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Delete Multiple Makes (Partial Failure)
|
|
||||||
```bash
|
|
||||||
DELETE /api/admin/catalog/makes/bulk-delete
|
|
||||||
{
|
|
||||||
"ids": [1, 2, 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
Response: 207 Multi-Status
|
|
||||||
{
|
|
||||||
"deleted": [3],
|
|
||||||
"failed": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"error": "Cannot delete make with existing models"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"error": "Make 2 not found"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Invalid Entity Type
|
|
||||||
```bash
|
|
||||||
DELETE /api/admin/catalog/invalid/bulk-delete
|
|
||||||
{
|
|
||||||
"ids": [1, 2, 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
Response: 400 Bad Request
|
|
||||||
{
|
|
||||||
"error": "Invalid entity type",
|
|
||||||
"message": "Entity must be one of: makes, models, years, trims, engines"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 4: Invalid IDs
|
|
||||||
```bash
|
|
||||||
DELETE /api/admin/catalog/makes/bulk-delete
|
|
||||||
{
|
|
||||||
"ids": ["abc", "def"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Response: 400 Bad Request
|
|
||||||
{
|
|
||||||
"error": "Invalid IDs",
|
|
||||||
"message": "All IDs must be positive integers"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Files Modified
|
|
||||||
1. `/backend/src/features/admin/api/admin.routes.ts` (line 209-212)
|
|
||||||
- Added route: `DELETE /admin/catalog/:entity/bulk-delete`
|
|
||||||
- Requires admin authentication
|
|
||||||
|
|
||||||
2. `/backend/src/features/admin/api/catalog.controller.ts` (line 542-638)
|
|
||||||
- Added method: `bulkDeleteCatalogEntity()`
|
|
||||||
- Maps entity type to appropriate delete method
|
|
||||||
- Processes deletions sequentially
|
|
||||||
- Collects successes and failures
|
|
||||||
|
|
||||||
3. `/backend/src/features/admin/api/admin.validation.ts` (line 43-49, 57-58)
|
|
||||||
- Added `catalogEntitySchema`: Validates entity type
|
|
||||||
- Added `bulkDeleteCatalogSchema`: Validates request body
|
|
||||||
- Exported types: `CatalogEntity`, `BulkDeleteCatalogInput`
|
|
||||||
|
|
||||||
4. `/backend/src/features/admin/domain/admin.types.ts` (line 97-103)
|
|
||||||
- Added `BulkDeleteCatalogResponse` interface
|
|
||||||
|
|
||||||
### Continue-on-Failure Pattern
|
|
||||||
The endpoint uses a continue-on-failure pattern:
|
|
||||||
- One deletion failure does NOT stop the batch
|
|
||||||
- All deletions are attempted
|
|
||||||
- Successes and failures are tracked separately
|
|
||||||
- Final response includes both lists
|
|
||||||
|
|
||||||
### Transaction Behavior
|
|
||||||
- Each individual deletion runs in its own transaction (via service layer)
|
|
||||||
- If one delete fails, it doesn't affect others
|
|
||||||
- No rollback of previously successful deletions
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Manual Testing with cURL
|
|
||||||
```bash
|
|
||||||
# Test valid request (requires auth token)
|
|
||||||
curl -X DELETE "http://localhost/api/admin/catalog/makes/bulk-delete" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"ids": [1, 2, 3]}'
|
|
||||||
|
|
||||||
# Test invalid entity type
|
|
||||||
curl -X DELETE "http://localhost/api/admin/catalog/invalid/bulk-delete" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"ids": [1, 2, 3]}'
|
|
||||||
|
|
||||||
# Test empty IDs
|
|
||||||
curl -X DELETE "http://localhost/api/admin/catalog/makes/bulk-delete" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"ids": []}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Expected Audit Log Behavior
|
|
||||||
Each successful deletion creates a platform change log entry:
|
|
||||||
- `changeType`: "DELETE"
|
|
||||||
- `resourceType`: Entity type (makes, models, years, trims, engines)
|
|
||||||
- `resourceId`: ID of deleted entity
|
|
||||||
- `changedBy`: Actor's user ID
|
|
||||||
- `oldValue`: Entity data before deletion
|
|
||||||
- `newValue`: null
|
|
||||||
|
|
||||||
## Security
|
|
||||||
- Endpoint requires admin authentication (via `fastify.requireAdmin`)
|
|
||||||
- Actor ID is logged for all operations
|
|
||||||
- All deletions are audited in platform_change_log table
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
- Deletions are processed sequentially (not in parallel)
|
|
||||||
- Each deletion queries the database separately
|
|
||||||
- Cache invalidation occurs after each successful deletion
|
|
||||||
- For large batches (50+ items), consider breaking into smaller batches
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
Potential improvements:
|
|
||||||
1. Add cascade delete option to automatically delete dependent entities
|
|
||||||
2. Add dry-run mode to preview what would be deleted
|
|
||||||
3. Add batch size optimization for better performance
|
|
||||||
4. Add progress tracking for long-running batches
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
# Community Gas Stations Feature - Implementation Complete
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The Community Gas Stations feature has been fully implemented as a complete backend feature capsule for MotoVaultPro. This feature allows users to submit 93 octane gas station locations that require admin approval before becoming publicly visible. The implementation follows the modular monolith architecture with self-contained feature capsules.
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
### Domain Layer (Business Logic)
|
|
||||||
|
|
||||||
1. **`backend/src/features/stations/domain/community-stations.types.ts`**
|
|
||||||
- Complete TypeScript type definitions
|
|
||||||
- Interfaces for CommunityStation, submission body, review body, filters
|
|
||||||
- Supports all business requirements
|
|
||||||
|
|
||||||
2. **`backend/src/features/stations/domain/community-stations.service.ts`**
|
|
||||||
- Complete business logic layer
|
|
||||||
- Methods: submitStation, getMySubmissions, withdrawSubmission, getApprovedStations, getApprovedNearby, getPendingReview, reviewStation
|
|
||||||
- Redis caching with 5-minute TTL for approved stations and nearby searches
|
|
||||||
- Proper error handling and validation
|
|
||||||
- User ownership checks on sensitive operations
|
|
||||||
|
|
||||||
### Data Access Layer
|
|
||||||
|
|
||||||
3. **`backend/src/features/stations/data/community-stations.repository.ts`**
|
|
||||||
- Repository pattern with parameterized SQL queries (no string concatenation)
|
|
||||||
- Methods: submitStation, getStationById, getUserSubmissions, getApprovedStations, getNearbyApprovedStations, getPendingStations, getAllStationsWithFilters, reviewStation, deleteStation
|
|
||||||
- Proper row mapping to domain types
|
|
||||||
- All queries use prepared statements for security
|
|
||||||
|
|
||||||
### API Layer
|
|
||||||
|
|
||||||
4. **`backend/src/features/stations/api/community-stations.controller.ts`**
|
|
||||||
- Complete HTTP request handler class
|
|
||||||
- Methods for all user and admin endpoints
|
|
||||||
- Proper error handling with meaningful error codes
|
|
||||||
- Request validation delegation to schemas
|
|
||||||
- Audit logging for admin actions
|
|
||||||
|
|
||||||
5. **`backend/src/features/stations/api/community-stations.validation.ts`**
|
|
||||||
- Zod validation schemas for all requests
|
|
||||||
- SubmitCommunityStationSchema, ReviewStationSchema, FiltersSchema, NearbySchema
|
|
||||||
- Type-safe input validation
|
|
||||||
- Clear error messages for validation failures
|
|
||||||
|
|
||||||
6. **`backend/src/features/stations/api/community-stations.routes.ts`**
|
|
||||||
- Fastify plugin for route registration
|
|
||||||
- User routes: POST /api/stations/community, GET /api/stations/community/mine, DELETE /api/stations/community/:id, GET /api/stations/community/approved, POST /api/stations/community/nearby
|
|
||||||
- Admin routes integrated into admin.routes.ts
|
|
||||||
- Proper authentication and authorization guards
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
7. **`backend/src/features/stations/tests/unit/community-stations.service.test.ts`**
|
|
||||||
- 40+ test cases covering service layer
|
|
||||||
- Tests for all service methods
|
|
||||||
- Cache invalidation testing
|
|
||||||
- Error condition testing
|
|
||||||
- User ownership validation tests
|
|
||||||
|
|
||||||
8. **`backend/src/features/stations/tests/integration/community-stations.api.test.ts`**
|
|
||||||
- Integration tests for complete API workflows
|
|
||||||
- Tests for HTTP endpoints
|
|
||||||
- Database interaction verification
|
|
||||||
- Authentication and authorization tests
|
|
||||||
- Error response validation
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
9. **`backend/src/features/stations/COMMUNITY-STATIONS.md`**
|
|
||||||
- Complete feature documentation
|
|
||||||
- API endpoint specifications with examples
|
|
||||||
- Database schema documentation
|
|
||||||
- Validation rules
|
|
||||||
- Caching strategy
|
|
||||||
- Error codes
|
|
||||||
- Development notes
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### App Registration
|
|
||||||
- **File**: `backend/src/app.ts`
|
|
||||||
- **Changes**: Added import and registration of communityStationsRoutes
|
|
||||||
- **Status**: Complete
|
|
||||||
|
|
||||||
### Admin Routes Integration
|
|
||||||
- **File**: `backend/src/features/admin/api/admin.routes.ts`
|
|
||||||
- **Changes**:
|
|
||||||
- Added CommunityStationsController import
|
|
||||||
- Instantiated controller in route handler
|
|
||||||
- Added 3 admin endpoints for community station management
|
|
||||||
- Integrated into Phase 5 of admin operations
|
|
||||||
- **Status**: Complete
|
|
||||||
|
|
||||||
### Stations Feature Index
|
|
||||||
- **File**: `backend/src/features/stations/index.ts`
|
|
||||||
- **Changes**:
|
|
||||||
- Exported CommunityStationsService
|
|
||||||
- Exported all community station types
|
|
||||||
- Exported route definitions
|
|
||||||
- **Status**: Complete
|
|
||||||
|
|
||||||
### Database
|
|
||||||
- **File**: `backend/src/features/stations/migrations/004_create_community_stations.sql`
|
|
||||||
- **Status**: Already exists, no changes needed
|
|
||||||
- **Includes**:
|
|
||||||
- community_stations table with all required columns
|
|
||||||
- Indexes for common queries
|
|
||||||
- Trigger for updated_at timestamp
|
|
||||||
|
|
||||||
## API Endpoints Implemented
|
|
||||||
|
|
||||||
### User Endpoints (JWT Required)
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|--------|------|---------|
|
|
||||||
| POST | /api/stations/community | Submit new station |
|
|
||||||
| GET | /api/stations/community/mine | Get user's submissions |
|
|
||||||
| DELETE | /api/stations/community/:id | Withdraw pending submission |
|
|
||||||
| GET | /api/stations/community/approved | List approved stations |
|
|
||||||
| POST | /api/stations/community/nearby | Find nearby approved stations |
|
|
||||||
|
|
||||||
### Admin Endpoints (Admin Role Required)
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|--------|------|---------|
|
|
||||||
| GET | /api/admin/community-stations | List all submissions with filters |
|
|
||||||
| GET | /api/admin/community-stations/pending | Get pending review queue |
|
|
||||||
| PATCH | /api/admin/community-stations/:id/review | Approve or reject submission |
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **User Submission**: Users can submit gas station locations with optional fuel details
|
|
||||||
- **Admin Approval Workflow**: Submissions start as "pending" and require admin review
|
|
||||||
- **Public Listing**: Approved stations are visible to all authenticated users
|
|
||||||
- **Location Search**: Find approved stations near specific coordinates
|
|
||||||
- **User Withdrawal**: Users can withdraw their own pending submissions
|
|
||||||
- **Audit Logging**: All admin actions are logged with context
|
|
||||||
- **Caching**: Redis caching for approved stations and location-based searches
|
|
||||||
- **Validation**: Comprehensive input validation using Zod
|
|
||||||
- **Error Handling**: Meaningful error messages and proper HTTP status codes
|
|
||||||
- **User Ownership**: All user operations validate ownership of their submissions
|
|
||||||
- **Type Safety**: Full TypeScript support with strict typing
|
|
||||||
|
|
||||||
## Quality Metrics
|
|
||||||
|
|
||||||
### Type Safety
|
|
||||||
- ✅ Zero TypeScript errors
|
|
||||||
- ✅ Strict type checking enabled
|
|
||||||
- ✅ Full type definitions for all features
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
- ✅ No errors from new code
|
|
||||||
- ✅ Follows existing code style
|
|
||||||
- ✅ ESLint compliant
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- ✅ Unit tests for service layer (40+ tests)
|
|
||||||
- ✅ Integration tests for API endpoints
|
|
||||||
- ✅ Error condition coverage
|
|
||||||
- ✅ Authorization/authentication testing
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- ✅ Feature documentation complete
|
|
||||||
- ✅ API specifications with examples
|
|
||||||
- ✅ Database schema documented
|
|
||||||
- ✅ Validation rules documented
|
|
||||||
- ✅ Development notes included
|
|
||||||
|
|
||||||
## Architecture Compliance
|
|
||||||
|
|
||||||
### Modular Monolith Pattern
|
|
||||||
- ✅ Feature fully contained in `backend/src/features/stations/`
|
|
||||||
- ✅ Repository pattern for data access
|
|
||||||
- ✅ Service layer for business logic
|
|
||||||
- ✅ Controller layer for HTTP handling
|
|
||||||
- ✅ No direct database access from controllers
|
|
||||||
- ✅ No business logic in controllers
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- ✅ JWT authentication required for all user endpoints
|
|
||||||
- ✅ Admin role required for admin endpoints
|
|
||||||
- ✅ User ownership validation on sensitive operations
|
|
||||||
- ✅ Parameterized SQL queries (no string concatenation)
|
|
||||||
- ✅ Input validation on all requests
|
|
||||||
- ✅ Meaningful error messages without exposing internals
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- ✅ Redis caching for frequently accessed data
|
|
||||||
- ✅ Proper database indexes on common query fields
|
|
||||||
- ✅ Location-based search with distance calculation
|
|
||||||
- ✅ Pagination support for large result sets
|
|
||||||
- ✅ Efficient cache invalidation strategy
|
|
||||||
|
|
||||||
## Database Indexes
|
|
||||||
|
|
||||||
The migration creates indexes for:
|
|
||||||
- `status` - For filtering by submission status
|
|
||||||
- `latitude, longitude` - For location-based searches
|
|
||||||
- `submitted_by` - For user-specific queries
|
|
||||||
- `created_at DESC` - For sorting by submission time
|
|
||||||
|
|
||||||
## Caching Strategy
|
|
||||||
|
|
||||||
- **Approved Stations List**: 5-minute TTL, cache key includes pagination params
|
|
||||||
- **Nearby Stations**: 5-minute TTL, cache key includes coordinates and radius
|
|
||||||
- **Invalidation**: Caches cleared on new submissions and admin reviews
|
|
||||||
- **Pattern**: `mvp:community-stations:*` for cache keys
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
| Status | Code | Use Case |
|
|
||||||
|--------|------|----------|
|
|
||||||
| 201 | Created | Successful submission |
|
|
||||||
| 204 | No Content | Successful deletion/withdrawal |
|
|
||||||
| 400 | Bad Request | Validation error or invalid operation |
|
|
||||||
| 401 | Unauthorized | Missing authentication |
|
|
||||||
| 403 | Forbidden | Missing admin role |
|
|
||||||
| 404 | Not Found | Station not found |
|
|
||||||
| 500 | Server Error | Unexpected error |
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### For Frontend Integration
|
|
||||||
|
|
||||||
1. **User Submission Flow**
|
|
||||||
- Use `POST /api/stations/community` endpoint
|
|
||||||
- Provide validation feedback from schema errors
|
|
||||||
- Show success/error messages
|
|
||||||
|
|
||||||
2. **View Submissions**
|
|
||||||
- Use `GET /api/stations/community/mine` with pagination
|
|
||||||
- Display station status (pending/approved/rejected)
|
|
||||||
- Show rejection reasons if applicable
|
|
||||||
- Provide withdrawal option for pending submissions
|
|
||||||
|
|
||||||
3. **Discover Stations**
|
|
||||||
- Use `GET /api/stations/community/approved` for list view
|
|
||||||
- Use `POST /api/stations/community/nearby` for map view
|
|
||||||
- Display station details and user-added notes
|
|
||||||
|
|
||||||
### For Admin Integration
|
|
||||||
|
|
||||||
1. **Review Queue**
|
|
||||||
- Use `GET /api/admin/community-stations/pending` to get submissions
|
|
||||||
- Display station details and user notes
|
|
||||||
- Provide approve/reject interface
|
|
||||||
|
|
||||||
2. **Review Submission**
|
|
||||||
- Use `PATCH /api/admin/community-stations/:id/review`
|
|
||||||
- For approval: send `{status: "approved"}`
|
|
||||||
- For rejection: send `{status: "rejected", rejectionReason: "..."}`
|
|
||||||
- Handle success/error responses
|
|
||||||
|
|
||||||
3. **Filter Submissions**
|
|
||||||
- Use `GET /api/admin/community-stations?status=approved` etc.
|
|
||||||
- Support filtering by status and submitter
|
|
||||||
- Pagination support for large result sets
|
|
||||||
|
|
||||||
## Testing Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run unit tests
|
|
||||||
npm test -- features/stations/tests/unit/community-stations.service.test.ts
|
|
||||||
|
|
||||||
# Run integration tests
|
|
||||||
npm test -- features/stations/tests/integration/community-stations.api.test.ts
|
|
||||||
|
|
||||||
# Type check
|
|
||||||
npm run type-check
|
|
||||||
|
|
||||||
# Lint
|
|
||||||
npm run lint
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Deployment
|
|
||||||
|
|
||||||
The feature is fully integrated into the existing Docker setup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rebuild containers after code changes
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# Run tests in container
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
make logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Feature Completeness Checklist
|
|
||||||
|
|
||||||
- ✅ User can submit gas stations
|
|
||||||
- ✅ Submissions require admin approval
|
|
||||||
- ✅ Approved stations are publicly visible
|
|
||||||
- ✅ User can withdraw pending submissions
|
|
||||||
- ✅ User can find nearby approved stations
|
|
||||||
- ✅ Admin can review pending submissions
|
|
||||||
- ✅ Admin can approve or reject with reason
|
|
||||||
- ✅ All operations are audited
|
|
||||||
- ✅ Proper error handling
|
|
||||||
- ✅ Input validation
|
|
||||||
- ✅ User ownership validation
|
|
||||||
- ✅ Caching for performance
|
|
||||||
- ✅ Database indexes for query optimization
|
|
||||||
- ✅ Complete API documentation
|
|
||||||
- ✅ Unit and integration tests
|
|
||||||
- ✅ Type safety with TypeScript
|
|
||||||
- ✅ Follows modular monolith pattern
|
|
||||||
- ✅ Zero linting errors
|
|
||||||
- ✅ Zero type errors
|
|
||||||
- ✅ Ready for production deployment
|
|
||||||
|
|
||||||
## File Summary
|
|
||||||
|
|
||||||
| File | Lines | Purpose |
|
|
||||||
|------|-------|---------|
|
|
||||||
| community-stations.types.ts | 56 | Type definitions |
|
|
||||||
| community-stations.service.ts | 125 | Business logic |
|
|
||||||
| community-stations.repository.ts | 283 | Data access |
|
|
||||||
| community-stations.controller.ts | 226 | HTTP handlers |
|
|
||||||
| community-stations.validation.ts | 58 | Input schemas |
|
|
||||||
| community-stations.routes.ts | 65 | Route definitions |
|
|
||||||
| community-stations.service.test.ts | 242 | Service tests |
|
|
||||||
| community-stations.api.test.ts | 292 | Integration tests |
|
|
||||||
| COMMUNITY-STATIONS.md | 390 | Feature documentation |
|
|
||||||
| **Total** | **1,737** | **Complete feature** |
|
|
||||||
|
|
||||||
## Handoff Status
|
|
||||||
|
|
||||||
The feature is complete and ready for:
|
|
||||||
- ✅ Frontend team for mobile and desktop integration
|
|
||||||
- ✅ Quality Assurance for validation testing
|
|
||||||
- ✅ Production deployment with existing infrastructure
|
|
||||||
|
|
||||||
All code has been tested, linted, type-checked, and documented according to MotoVaultPro standards.
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# Community Gas Stations - All Files Created
|
|
||||||
|
|
||||||
Complete list of all files created for the community gas stations feature with full mobile + desktop implementation.
|
|
||||||
|
|
||||||
## User Features - Stations Module
|
|
||||||
|
|
||||||
### Types (1)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/types/community-stations.types.ts`
|
|
||||||
|
|
||||||
### API Client (1)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/api/community-stations.api.ts`
|
|
||||||
|
|
||||||
### Hooks (2)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/hooks/useCommunityStations.ts`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/hooks/index-community.ts`
|
|
||||||
|
|
||||||
### Components (4)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/CommunityStationCard.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/SubmitStationForm.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/CommunityStationsList.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/index-community.ts`
|
|
||||||
|
|
||||||
### Pages & Screens (2)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/pages/CommunityStationsPage.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/mobile/CommunityStationsMobileScreen.tsx`
|
|
||||||
|
|
||||||
### Tests (3)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/api/community-stations.api.test.ts`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/hooks/useCommunityStations.test.ts`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/components/CommunityStationCard.test.tsx`
|
|
||||||
|
|
||||||
### Documentation (1)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/COMMUNITY-STATIONS-README.md`
|
|
||||||
|
|
||||||
## Admin Features - Admin Module
|
|
||||||
|
|
||||||
### Components (2)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/components/CommunityStationReviewCard.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/components/CommunityStationReviewQueue.tsx`
|
|
||||||
|
|
||||||
### Pages & Screens (2)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/mobile/AdminCommunityStationsMobileScreen.tsx`
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
### Implementation Summary (2)
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/COMMUNITY-STATIONS-IMPLEMENTATION.md`
|
|
||||||
- `/Users/egullickson/Documents/Technology/coding/motovaultpro/COMMUNITY-STATIONS-FILES.md` (this file)
|
|
||||||
|
|
||||||
## Total Files
|
|
||||||
|
|
||||||
18 source files + 2 documentation files = **20 total files created**
|
|
||||||
|
|
||||||
## File Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend/src/features/
|
|
||||||
├── stations/
|
|
||||||
│ ├── types/
|
|
||||||
│ │ └── community-stations.types.ts (1 file)
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── community-stations.api.ts (1 file)
|
|
||||||
│ ├── hooks/
|
|
||||||
│ │ ├── useCommunityStations.ts (1 file)
|
|
||||||
│ │ └── index-community.ts (1 file)
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── CommunityStationCard.tsx (1 file)
|
|
||||||
│ │ ├── SubmitStationForm.tsx (1 file)
|
|
||||||
│ │ ├── CommunityStationsList.tsx (1 file)
|
|
||||||
│ │ └── index-community.ts (1 file)
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── CommunityStationsPage.tsx (1 file)
|
|
||||||
│ ├── mobile/
|
|
||||||
│ │ └── CommunityStationsMobileScreen.tsx (1 file)
|
|
||||||
│ ├── __tests__/
|
|
||||||
│ │ ├── api/
|
|
||||||
│ │ │ └── community-stations.api.test.ts (1 file)
|
|
||||||
│ │ ├── hooks/
|
|
||||||
│ │ │ └── useCommunityStations.test.ts (1 file)
|
|
||||||
│ │ └── components/
|
|
||||||
│ │ └── CommunityStationCard.test.tsx (1 file)
|
|
||||||
│ └── COMMUNITY-STATIONS-README.md (1 file)
|
|
||||||
│
|
|
||||||
└── admin/
|
|
||||||
├── components/
|
|
||||||
│ ├── CommunityStationReviewCard.tsx (1 file)
|
|
||||||
│ └── CommunityStationReviewQueue.tsx (1 file)
|
|
||||||
├── pages/
|
|
||||||
│ └── AdminCommunityStationsPage.tsx (1 file)
|
|
||||||
└── mobile/
|
|
||||||
└── AdminCommunityStationsMobileScreen.tsx (1 file)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Links to Key Files
|
|
||||||
|
|
||||||
### User Interface
|
|
||||||
- Desktop: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/pages/CommunityStationsPage.tsx`
|
|
||||||
- Mobile: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/mobile/CommunityStationsMobileScreen.tsx`
|
|
||||||
- Form: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/SubmitStationForm.tsx`
|
|
||||||
|
|
||||||
### Admin Interface
|
|
||||||
- Desktop: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx`
|
|
||||||
- Mobile: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/mobile/AdminCommunityStationsMobileScreen.tsx`
|
|
||||||
- Review Card: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/components/CommunityStationReviewCard.tsx`
|
|
||||||
|
|
||||||
### Data & API
|
|
||||||
- Types: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/types/community-stations.types.ts`
|
|
||||||
- API: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/api/community-stations.api.ts`
|
|
||||||
- Hooks: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/hooks/useCommunityStations.ts`
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
- API Tests: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/api/community-stations.api.test.ts`
|
|
||||||
- Hook Tests: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/hooks/useCommunityStations.test.ts`
|
|
||||||
- Component Tests: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/__tests__/components/CommunityStationCard.test.tsx`
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Feature Guide: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/COMMUNITY-STATIONS-README.md`
|
|
||||||
- Implementation Summary: `/Users/egullickson/Documents/Technology/coding/motovaultpro/COMMUNITY-STATIONS-IMPLEMENTATION.md`
|
|
||||||
|
|
||||||
## Features Implemented
|
|
||||||
|
|
||||||
### User Features
|
|
||||||
- Submit 93 octane gas station with geolocation
|
|
||||||
- Browse approved community stations
|
|
||||||
- View personal submissions with status
|
|
||||||
- Browse nearby approved stations
|
|
||||||
- Withdraw pending submissions
|
|
||||||
- Full form validation
|
|
||||||
|
|
||||||
### Admin Features
|
|
||||||
- Review pending submissions
|
|
||||||
- Approve/reject with reasons
|
|
||||||
- Bulk operations support
|
|
||||||
- Filter by status
|
|
||||||
- View statistics
|
|
||||||
- Complete audit trail
|
|
||||||
|
|
||||||
### Quality Assurance
|
|
||||||
- TypeScript strict mode
|
|
||||||
- React Query integration
|
|
||||||
- Zod validation
|
|
||||||
- Comprehensive error handling
|
|
||||||
- Loading states
|
|
||||||
- 100% mobile + desktop parity
|
|
||||||
- Unit tests
|
|
||||||
- API mocking tests
|
|
||||||
|
|
||||||
## Mobile + Desktop Validation
|
|
||||||
|
|
||||||
All components tested and validated for:
|
|
||||||
- Mobile viewport (320px - 599px)
|
|
||||||
- Tablet viewport (600px - 1023px)
|
|
||||||
- Desktop viewport (1024px+)
|
|
||||||
- Touch interaction (44px+ targets)
|
|
||||||
- Keyboard navigation
|
|
||||||
- Form inputs
|
|
||||||
- Pagination
|
|
||||||
- Modal dialogs
|
|
||||||
- Tab navigation
|
|
||||||
|
|
||||||
## Build Status
|
|
||||||
|
|
||||||
✅ All files created successfully
|
|
||||||
✅ TypeScript compilation passes
|
|
||||||
✅ No linting errors
|
|
||||||
✅ Container builds successfully
|
|
||||||
✅ Frontend serving at https://admin.motovaultpro.com
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Implement backend API endpoints
|
|
||||||
2. Integrate routes into App.tsx
|
|
||||||
3. Update navigation menus
|
|
||||||
4. Run full test suite
|
|
||||||
5. Validate on mobile/desktop devices
|
|
||||||
6. Deploy and monitor
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
# Community Gas Stations Feature - Implementation Complete
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Complete implementation of the community gas station feature for MotoVaultPro with full mobile + desktop parity. Users can submit 93 octane gas stations for admin approval, and admins can review submissions in a workflow.
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
All components, hooks, pages, and screens have been implemented and tested.
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
### Types (1 file)
|
|
||||||
1. **frontend/src/features/stations/types/community-stations.types.ts**
|
|
||||||
- CommunityStation - Main entity
|
|
||||||
- SubmitStationData - Form submission data
|
|
||||||
- ReviewDecision - Admin review decision
|
|
||||||
- CommunityStationsListResponse - Paginated list response
|
|
||||||
|
|
||||||
### API Client (1 file)
|
|
||||||
2. **frontend/src/features/stations/api/community-stations.api.ts**
|
|
||||||
- User endpoints: submit, getMySubmissions, withdrawSubmission, getApprovedStations, getApprovedNearby
|
|
||||||
- Admin endpoints: getAllSubmissions, getPendingSubmissions, reviewStation, bulkReviewStations
|
|
||||||
|
|
||||||
### React Query Hooks (1 file)
|
|
||||||
3. **frontend/src/features/stations/hooks/useCommunityStations.ts**
|
|
||||||
- useSubmitStation() - Submit new station
|
|
||||||
- useMySubmissions() - Fetch user submissions
|
|
||||||
- useWithdrawSubmission() - Withdraw submission
|
|
||||||
- useApprovedStations() - Fetch approved stations
|
|
||||||
- useApprovedNearbyStations() - Fetch nearby approved stations
|
|
||||||
- useAllCommunitySubmissions() - Admin: all submissions
|
|
||||||
- usePendingSubmissions() - Admin: pending submissions
|
|
||||||
- useReviewStation() - Admin: review submission
|
|
||||||
- useBulkReviewStations() - Admin: bulk review
|
|
||||||
|
|
||||||
### User Components (3 files)
|
|
||||||
4. **frontend/src/features/stations/components/CommunityStationCard.tsx**
|
|
||||||
- Display individual community station with status, details, and actions
|
|
||||||
- Admin and user views with appropriate buttons
|
|
||||||
- Rejection reason dialog for admins
|
|
||||||
- Mobile-friendly with 44px+ touch targets
|
|
||||||
|
|
||||||
5. **frontend/src/features/stations/components/SubmitStationForm.tsx**
|
|
||||||
- React Hook Form with Zod validation
|
|
||||||
- Geolocation integration
|
|
||||||
- Address, location, 93 octane details, price, notes
|
|
||||||
- Mobile-first responsive layout
|
|
||||||
- Loading and error states
|
|
||||||
|
|
||||||
6. **frontend/src/features/stations/components/CommunityStationsList.tsx**
|
|
||||||
- Grid list of stations (1 col mobile, 2+ cols desktop)
|
|
||||||
- Pagination support
|
|
||||||
- Loading, error, and empty states
|
|
||||||
- Works for both user and admin views
|
|
||||||
|
|
||||||
### User Pages (2 files)
|
|
||||||
7. **frontend/src/features/stations/pages/CommunityStationsPage.tsx**
|
|
||||||
- Desktop page with tab navigation: Browse All, My Submissions, Near Me
|
|
||||||
- Submit dialog for new stations
|
|
||||||
- Integrates map view for nearby stations
|
|
||||||
- Responsive to mobile via media queries
|
|
||||||
|
|
||||||
8. **frontend/src/features/stations/mobile/CommunityStationsMobileScreen.tsx**
|
|
||||||
- Mobile-optimized with bottom tab navigation: Browse, Submit, My Submissions
|
|
||||||
- Full-screen submit form
|
|
||||||
- Pull-to-refresh support (structure prepared)
|
|
||||||
- Touch-friendly spacing and buttons
|
|
||||||
|
|
||||||
### Admin Components (2 files)
|
|
||||||
9. **frontend/src/features/admin/components/CommunityStationReviewCard.tsx**
|
|
||||||
- Individual station review card for admins
|
|
||||||
- Approve/reject buttons with actions
|
|
||||||
- Rejection reason dialog
|
|
||||||
- Station details, coordinates, notes, metadata
|
|
||||||
- 44px+ touch targets
|
|
||||||
|
|
||||||
10. **frontend/src/features/admin/components/CommunityStationReviewQueue.tsx**
|
|
||||||
- Queue of pending submissions for review
|
|
||||||
- Grid layout (1 col mobile, 2+ cols desktop)
|
|
||||||
- Pagination support
|
|
||||||
- Loading, error, and empty states
|
|
||||||
|
|
||||||
### Admin Pages (2 files)
|
|
||||||
11. **frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx**
|
|
||||||
- Desktop admin page with Pending and All submissions tabs
|
|
||||||
- Status filter dropdown for all submissions
|
|
||||||
- Review actions (approve/reject)
|
|
||||||
- Statistics dashboard
|
|
||||||
- Responsive to mobile
|
|
||||||
|
|
||||||
12. **frontend/src/features/admin/mobile/AdminCommunityStationsMobileScreen.tsx**
|
|
||||||
- Mobile admin interface with bottom tabs
|
|
||||||
- Status filter for all submissions tab
|
|
||||||
- Admin access control
|
|
||||||
- Touch-friendly review workflow
|
|
||||||
|
|
||||||
### Tests (3 files)
|
|
||||||
13. **frontend/src/features/stations/__tests__/components/CommunityStationCard.test.tsx**
|
|
||||||
- Rendering tests for station details
|
|
||||||
- User interaction tests
|
|
||||||
- Mobile viewport tests
|
|
||||||
- Admin/user view tests
|
|
||||||
|
|
||||||
14. **frontend/src/features/stations/__tests__/hooks/useCommunityStations.test.ts**
|
|
||||||
- React Query hook tests
|
|
||||||
- Mutation and query tests
|
|
||||||
- Mock API responses
|
|
||||||
- Error handling tests
|
|
||||||
|
|
||||||
15. **frontend/src/features/stations/__tests__/api/community-stations.api.test.ts**
|
|
||||||
- API client tests
|
|
||||||
- Successful and error requests
|
|
||||||
- Parameter validation
|
|
||||||
- Admin endpoint tests
|
|
||||||
|
|
||||||
### Exports and Documentation (2 files)
|
|
||||||
16. **frontend/src/features/stations/hooks/index-community.ts**
|
|
||||||
- Export all community stations hooks
|
|
||||||
|
|
||||||
17. **frontend/src/features/stations/components/index-community.ts**
|
|
||||||
- Export all community stations components
|
|
||||||
|
|
||||||
18. **frontend/src/features/stations/COMMUNITY-STATIONS-README.md**
|
|
||||||
- Complete feature documentation
|
|
||||||
- API endpoints
|
|
||||||
- Hook usage examples
|
|
||||||
- Component props
|
|
||||||
- Testing instructions
|
|
||||||
- Integration guide
|
|
||||||
|
|
||||||
## Key Features Implemented
|
|
||||||
|
|
||||||
### User Features
|
|
||||||
- Submit new 93 octane gas stations with:
|
|
||||||
- Station name, address, city/state/zip
|
|
||||||
- Brand (optional)
|
|
||||||
- 93 octane availability and ethanol-free options
|
|
||||||
- Price per gallon (optional)
|
|
||||||
- Additional notes (optional)
|
|
||||||
- Geolocation support (use current location button)
|
|
||||||
- Form validation with Zod
|
|
||||||
|
|
||||||
- Browse community submissions:
|
|
||||||
- All approved stations (paginated)
|
|
||||||
- Personal submissions with status
|
|
||||||
- Nearby stations (with geolocation)
|
|
||||||
- Station details with rejection reasons (if rejected)
|
|
||||||
- Withdraw pending submissions
|
|
||||||
|
|
||||||
- View submission status:
|
|
||||||
- Pending (under review)
|
|
||||||
- Approved (publicly visible)
|
|
||||||
- Rejected (with reason)
|
|
||||||
|
|
||||||
### Admin Features
|
|
||||||
- Review pending submissions:
|
|
||||||
- Card-based review interface
|
|
||||||
- Full station details and notes
|
|
||||||
- Approve or reject with optional reason
|
|
||||||
- Bulk operations
|
|
||||||
|
|
||||||
- Manage submissions:
|
|
||||||
- View all submissions with filtering
|
|
||||||
- Filter by status (pending, approved, rejected)
|
|
||||||
- Quick statistics dashboard
|
|
||||||
- Pagination support
|
|
||||||
|
|
||||||
### Mobile + Desktop Parity
|
|
||||||
- 100% feature parity between mobile and desktop
|
|
||||||
- Responsive components using MUI breakpoints
|
|
||||||
- Touch-friendly (44px+ buttons)
|
|
||||||
- Bottom tab navigation on mobile
|
|
||||||
- Modal dialogs for forms
|
|
||||||
- Optimized form inputs for mobile keyboards
|
|
||||||
- Full-screen forms on mobile
|
|
||||||
|
|
||||||
### Quality Assurance
|
|
||||||
- TypeScript strict mode
|
|
||||||
- Zod validation
|
|
||||||
- React Query for data management
|
|
||||||
- Comprehensive error handling
|
|
||||||
- Loading states with skeletons
|
|
||||||
- Toast notifications
|
|
||||||
- Accessibility (ARIA labels)
|
|
||||||
- Unit and integration tests
|
|
||||||
|
|
||||||
## Technical Stack
|
|
||||||
|
|
||||||
- React 18 with TypeScript
|
|
||||||
- Material-UI (MUI) components
|
|
||||||
- React Hook Form + Zod validation
|
|
||||||
- React Query (TanStack Query)
|
|
||||||
- Axios for API calls
|
|
||||||
- Jest + React Testing Library
|
|
||||||
- Tailwind CSS for utilities
|
|
||||||
|
|
||||||
## Mobile Responsiveness
|
|
||||||
|
|
||||||
### Touch Targets
|
|
||||||
- All interactive elements: 44px × 44px minimum
|
|
||||||
- Button padding and spacing
|
|
||||||
- Icon button sizing
|
|
||||||
- Checkbox and form input heights
|
|
||||||
|
|
||||||
### Responsive Breakpoints
|
|
||||||
- Mobile: 320px - 599px (1 column grid)
|
|
||||||
- Tablet: 600px - 1023px (2 column grid)
|
|
||||||
- Desktop: 1024px+ (3+ column grid)
|
|
||||||
|
|
||||||
### Mobile Optimizations
|
|
||||||
- Full-width forms
|
|
||||||
- Bottom tab navigation
|
|
||||||
- Vertical scrolling (no horizontal)
|
|
||||||
- Large touch targets
|
|
||||||
- Appropriate keyboard types (email, tel, number)
|
|
||||||
- No hover-only interactions
|
|
||||||
- Dialog forms go full-screen
|
|
||||||
|
|
||||||
## Backend API Requirements
|
|
||||||
|
|
||||||
The following endpoints must be implemented in the backend:
|
|
||||||
|
|
||||||
### User Endpoints
|
|
||||||
```
|
|
||||||
POST /api/stations/community/submit
|
|
||||||
GET /api/stations/community/mine
|
|
||||||
DELETE /api/stations/community/:id
|
|
||||||
GET /api/stations/community/approved?page=0&limit=50
|
|
||||||
POST /api/stations/community/nearby
|
|
||||||
```
|
|
||||||
|
|
||||||
### Admin Endpoints
|
|
||||||
```
|
|
||||||
GET /api/stations/community/admin/submissions?status=&page=0&limit=50
|
|
||||||
GET /api/stations/community/admin/pending?page=0&limit=50
|
|
||||||
PATCH /api/stations/community/admin/:id/review
|
|
||||||
POST /api/stations/community/admin/bulk-review
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Steps
|
|
||||||
|
|
||||||
1. **Update App.tsx routing**
|
|
||||||
```tsx
|
|
||||||
import { CommunityStationsPage } from './features/stations/pages/CommunityStationsPage';
|
|
||||||
import { CommunityStationsMobileScreen } from './features/stations/mobile/CommunityStationsMobileScreen';
|
|
||||||
import { AdminCommunityStationsPage } from './features/admin/pages/AdminCommunityStationsPage';
|
|
||||||
import { AdminCommunityStationsMobileScreen } from './features/admin/mobile/AdminCommunityStationsMobileScreen';
|
|
||||||
|
|
||||||
// Add routes
|
|
||||||
<Route path="/stations/community" element={<CommunityStationsPage />} />
|
|
||||||
<Route path="/mobile/stations/community" element={<CommunityStationsMobileScreen />} />
|
|
||||||
<Route path="/admin/community-stations" element={<AdminCommunityStationsPage />} />
|
|
||||||
<Route path="/mobile/admin/community-stations" element={<AdminCommunityStationsMobileScreen />} />
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update navigation menus**
|
|
||||||
- Add "Community Stations" to main navigation
|
|
||||||
- Add "Community Station Reviews" to admin navigation
|
|
||||||
|
|
||||||
3. **Backend API implementation**
|
|
||||||
- Implement all endpoints listed above
|
|
||||||
- Add community_stations table migrations
|
|
||||||
- Add approval workflow logic
|
|
||||||
- Add admin authorization checks
|
|
||||||
|
|
||||||
4. **Testing**
|
|
||||||
```bash
|
|
||||||
npm test -- features/stations/community
|
|
||||||
npm test -- features/admin/community
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Summary
|
|
||||||
|
|
||||||
Total Files Created: 18
|
|
||||||
|
|
||||||
- Types: 1
|
|
||||||
- API: 1
|
|
||||||
- Hooks: 1
|
|
||||||
- User Components: 3
|
|
||||||
- User Pages: 2
|
|
||||||
- Admin Components: 2
|
|
||||||
- Admin Pages: 2
|
|
||||||
- Tests: 3
|
|
||||||
- Exports/Documentation: 3
|
|
||||||
|
|
||||||
## Build Status
|
|
||||||
|
|
||||||
✅ Frontend builds successfully
|
|
||||||
✅ No TypeScript errors
|
|
||||||
✅ No ESLint warnings
|
|
||||||
✅ All components export correctly
|
|
||||||
✅ Tests compile without errors
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
- [ ] Run component tests: `npm test -- features/stations/community`
|
|
||||||
- [ ] Run API tests: `npm test -- features/stations/community/api`
|
|
||||||
- [ ] Run hook tests: `npm test -- features/stations/community/hooks`
|
|
||||||
- [ ] Test mobile viewport (320px width)
|
|
||||||
- [ ] Test desktop viewport (1920px width)
|
|
||||||
- [ ] Test form submission and validation
|
|
||||||
- [ ] Test admin approval workflow
|
|
||||||
- [ ] Test error states and edge cases
|
|
||||||
- [ ] Test loading states
|
|
||||||
- [ ] Test geolocation integration
|
|
||||||
- [ ] Test pagination
|
|
||||||
- [ ] Test status filtering
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Comprehensive documentation included in:
|
|
||||||
- `/frontend/src/features/stations/COMMUNITY-STATIONS-README.md`
|
|
||||||
|
|
||||||
Features documented:
|
|
||||||
- Component props and usage
|
|
||||||
- Hook usage and examples
|
|
||||||
- API endpoints
|
|
||||||
- Type definitions
|
|
||||||
- Integration guide
|
|
||||||
- Testing instructions
|
|
||||||
- Mobile considerations
|
|
||||||
- Future enhancements
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Implement backend API endpoints
|
|
||||||
2. Test all features in containerized environment
|
|
||||||
3. Validate mobile + desktop on physical devices
|
|
||||||
4. Add routes to navigation menus
|
|
||||||
5. Configure database migrations
|
|
||||||
6. Run full test suite
|
|
||||||
7. Deploy and monitor
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
# Community Gas Stations - Integration Guide
|
|
||||||
|
|
||||||
Quick reference for integrating the community gas stations feature into MotoVaultPro.
|
|
||||||
|
|
||||||
## 1. Add Routes to App.tsx
|
|
||||||
|
|
||||||
Add these imports at the top of your main App.tsx or routing file:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { CommunityStationsPage } from './features/stations/pages/CommunityStationsPage';
|
|
||||||
import { CommunityStationsMobileScreen } from './features/stations/mobile/CommunityStationsMobileScreen';
|
|
||||||
import { AdminCommunityStationsPage } from './features/admin/pages/AdminCommunityStationsPage';
|
|
||||||
import { AdminCommunityStationsMobileScreen } from './features/admin/mobile/AdminCommunityStationsMobileScreen';
|
|
||||||
```
|
|
||||||
|
|
||||||
Add these routes in your route configuration:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// User routes
|
|
||||||
<Route path="/stations/community" element={<CommunityStationsPage />} />
|
|
||||||
<Route path="/mobile/stations/community" element={<CommunityStationsMobileScreen />} />
|
|
||||||
|
|
||||||
// Admin routes
|
|
||||||
<Route path="/admin/community-stations" element={<AdminCommunityStationsPage />} />
|
|
||||||
<Route path="/mobile/admin/community-stations" element={<AdminCommunityStationsMobileScreen />} />
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Update Navigation
|
|
||||||
|
|
||||||
### User Navigation
|
|
||||||
Add link to Community Stations:
|
|
||||||
```tsx
|
|
||||||
<NavLink to="/stations/community">
|
|
||||||
Community Stations
|
|
||||||
</NavLink>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Admin Navigation
|
|
||||||
Add link to Admin Community Stations:
|
|
||||||
```tsx
|
|
||||||
<NavLink to="/admin/community-stations">
|
|
||||||
Community Station Reviews
|
|
||||||
</NavLink>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Backend API Implementation
|
|
||||||
|
|
||||||
### Required Endpoints
|
|
||||||
|
|
||||||
Implement these endpoints in your backend API:
|
|
||||||
|
|
||||||
#### User Endpoints
|
|
||||||
```
|
|
||||||
POST /api/stations/community/submit
|
|
||||||
Body: SubmitStationData
|
|
||||||
Response: CommunityStation
|
|
||||||
|
|
||||||
GET /api/stations/community/mine
|
|
||||||
Response: CommunityStation[]
|
|
||||||
|
|
||||||
DELETE /api/stations/community/:id
|
|
||||||
Response: 204 No Content
|
|
||||||
|
|
||||||
GET /api/stations/community/approved?page=0&limit=50
|
|
||||||
Response: { stations: CommunityStation[], total: number, page: number, limit: number }
|
|
||||||
|
|
||||||
POST /api/stations/community/nearby
|
|
||||||
Body: { latitude, longitude, radiusMeters }
|
|
||||||
Response: CommunityStation[]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Admin Endpoints
|
|
||||||
```
|
|
||||||
GET /api/stations/community/admin/submissions?status=&page=0&limit=50
|
|
||||||
Response: CommunityStationsListResponse
|
|
||||||
|
|
||||||
GET /api/stations/community/admin/pending?page=0&limit=50
|
|
||||||
Response: CommunityStationsListResponse
|
|
||||||
|
|
||||||
PATCH /api/stations/community/admin/:id/review
|
|
||||||
Body: { status: 'approved' | 'rejected', rejectionReason?: string }
|
|
||||||
Response: CommunityStation
|
|
||||||
|
|
||||||
POST /api/stations/community/admin/bulk-review
|
|
||||||
Body: { ids: string[], status: 'approved' | 'rejected', rejectionReason?: string }
|
|
||||||
Response: CommunityStation[]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
|
|
||||||
The backend should have migrations for the `community_stations` table:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE community_stations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
||||||
submitted_by VARCHAR(255) NOT NULL,
|
|
||||||
name VARCHAR(200) NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
city VARCHAR(100),
|
|
||||||
state VARCHAR(50),
|
|
||||||
zip_code VARCHAR(20),
|
|
||||||
latitude DECIMAL(10, 8) NOT NULL,
|
|
||||||
longitude DECIMAL(11, 8) NOT NULL,
|
|
||||||
brand VARCHAR(100),
|
|
||||||
has_93_octane BOOLEAN DEFAULT true,
|
|
||||||
has_93_octane_ethanol_free BOOLEAN DEFAULT false,
|
|
||||||
price_93 DECIMAL(5, 3),
|
|
||||||
notes TEXT,
|
|
||||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
|
|
||||||
reviewed_by VARCHAR(255),
|
|
||||||
reviewed_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
rejection_reason TEXT,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_community_stations_status ON community_stations(status);
|
|
||||||
CREATE INDEX idx_community_stations_submitted_by ON community_stations(submitted_by);
|
|
||||||
CREATE INDEX idx_community_stations_location ON community_stations(latitude, longitude);
|
|
||||||
CREATE INDEX idx_community_stations_created_at ON community_stations(created_at DESC);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Environment Variables
|
|
||||||
|
|
||||||
No additional environment variables required. The API client uses the existing `VITE_API_BASE_URL`.
|
|
||||||
|
|
||||||
## 5. Testing
|
|
||||||
|
|
||||||
### Run Component Tests
|
|
||||||
```bash
|
|
||||||
npm test -- features/stations/community
|
|
||||||
npm test -- features/admin/community
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Mobile Viewport
|
|
||||||
```bash
|
|
||||||
# Open DevTools and set viewport to 375x667 (mobile)
|
|
||||||
# Test on: Browse, Submit, My Submissions tabs
|
|
||||||
# Verify: Form submission, withdrawal, pagination
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Desktop Viewport
|
|
||||||
```bash
|
|
||||||
# Open on 1920x1080 (desktop)
|
|
||||||
# Test on: Browse All, My Submissions, Near Me tabs
|
|
||||||
# Verify: Submit dialog, status filtering, nearby stations
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Manual Testing Checklist
|
|
||||||
|
|
||||||
### User Features
|
|
||||||
- [ ] Navigate to /stations/community
|
|
||||||
- [ ] Submit new station with geolocation
|
|
||||||
- [ ] Submit station with manual coordinates
|
|
||||||
- [ ] Submit station with all optional fields
|
|
||||||
- [ ] View approved stations
|
|
||||||
- [ ] View personal submissions
|
|
||||||
- [ ] Withdraw pending submission
|
|
||||||
- [ ] View rejected submission with reason
|
|
||||||
- [ ] Browse nearby approved stations
|
|
||||||
- [ ] Test pagination
|
|
||||||
- [ ] Test form validation (missing fields)
|
|
||||||
- [ ] Test location permission denied
|
|
||||||
- [ ] Test on mobile viewport
|
|
||||||
- [ ] Test on desktop viewport
|
|
||||||
- [ ] Test tab switching
|
|
||||||
|
|
||||||
### Admin Features
|
|
||||||
- [ ] Navigate to /admin/community-stations
|
|
||||||
- [ ] View pending submissions
|
|
||||||
- [ ] Approve submission
|
|
||||||
- [ ] Reject submission with reason
|
|
||||||
- [ ] Filter submissions by status
|
|
||||||
- [ ] View all submissions
|
|
||||||
- [ ] View approval statistics
|
|
||||||
- [ ] Test pagination
|
|
||||||
- [ ] Test on mobile viewport
|
|
||||||
- [ ] Test on desktop viewport
|
|
||||||
- [ ] Test tab switching
|
|
||||||
- [ ] Verify admin-only access
|
|
||||||
|
|
||||||
## 7. Deployment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
1. Backend API endpoints implemented
|
|
||||||
2. Database migrations applied
|
|
||||||
3. Admin role configured in authentication
|
|
||||||
4. Test on staging environment
|
|
||||||
|
|
||||||
### Deployment Steps
|
|
||||||
1. Merge to main branch
|
|
||||||
2. Run full test suite
|
|
||||||
3. Build and deploy frontend
|
|
||||||
4. Verify routes are accessible
|
|
||||||
5. Monitor logs for errors
|
|
||||||
6. Test on mobile and desktop
|
|
||||||
|
|
||||||
## 8. Monitoring
|
|
||||||
|
|
||||||
### Key Metrics
|
|
||||||
- Form submission success rate
|
|
||||||
- Approval/rejection ratio
|
|
||||||
- Pending submissions count
|
|
||||||
- Error rate on API endpoints
|
|
||||||
- Mobile vs desktop usage
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Form submission fails**
|
|
||||||
- Check backend API endpoints are implemented
|
|
||||||
- Verify JWT authentication is working
|
|
||||||
- Check CORS settings
|
|
||||||
|
|
||||||
**Geolocation not working**
|
|
||||||
- Check browser permissions
|
|
||||||
- Test on HTTPS only (required for geolocation)
|
|
||||||
- Verify GPS access on mobile device
|
|
||||||
|
|
||||||
**Admin endpoints return 403**
|
|
||||||
- Verify user has admin role
|
|
||||||
- Check authentication token is valid
|
|
||||||
- Check admin authorization middleware
|
|
||||||
|
|
||||||
**Images/photos not loading**
|
|
||||||
- Verify station photo API endpoints
|
|
||||||
- Check CloudFront/CDN cache
|
|
||||||
- Check CORS headers
|
|
||||||
|
|
||||||
## 9. Performance Optimization
|
|
||||||
|
|
||||||
### Implemented
|
|
||||||
- React Query caching
|
|
||||||
- Lazy loading of routes
|
|
||||||
- Code splitting
|
|
||||||
- Image optimization
|
|
||||||
|
|
||||||
### Optional Enhancements
|
|
||||||
- Implement infinite scroll for stations list
|
|
||||||
- Add offline support with service workers
|
|
||||||
- Implement map tile caching
|
|
||||||
- Add predictive prefetching
|
|
||||||
|
|
||||||
## 10. Security Considerations
|
|
||||||
|
|
||||||
### Already Implemented
|
|
||||||
- JWT authentication on all endpoints
|
|
||||||
- User-scoped data isolation
|
|
||||||
- Admin role-based access control
|
|
||||||
- Form validation (Zod)
|
|
||||||
- Input sanitization via axios
|
|
||||||
|
|
||||||
### Verify
|
|
||||||
- SQL injection prevention (parameterized queries)
|
|
||||||
- XSS prevention (React's built-in escaping)
|
|
||||||
- CSRF token validation
|
|
||||||
- Rate limiting on API endpoints
|
|
||||||
- Admin operations audit logging
|
|
||||||
|
|
||||||
## Quick Troubleshooting
|
|
||||||
|
|
||||||
### Components not rendering
|
|
||||||
1. Check routes are added to App.tsx
|
|
||||||
2. Verify imports are correct
|
|
||||||
3. Check browser console for errors
|
|
||||||
4. Verify React Query is initialized
|
|
||||||
|
|
||||||
### API calls failing
|
|
||||||
1. Check backend endpoints are implemented
|
|
||||||
2. Verify base URL is correct (VITE_API_BASE_URL)
|
|
||||||
3. Check authentication token is included
|
|
||||||
4. Verify CORS headers
|
|
||||||
|
|
||||||
### Tests failing
|
|
||||||
1. Mock API responses correctly
|
|
||||||
2. Use React Query's test utilities
|
|
||||||
3. Check for missing wait() calls
|
|
||||||
4. Verify Zod schema matches types
|
|
||||||
|
|
||||||
### Mobile layout broken
|
|
||||||
1. Check viewport settings
|
|
||||||
2. Verify MUI breakpoints are used
|
|
||||||
3. Check responsive classes
|
|
||||||
4. Test on actual mobile device
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For detailed documentation, see:
|
|
||||||
- `/frontend/src/features/stations/COMMUNITY-STATIONS-README.md`
|
|
||||||
- `/COMMUNITY-STATIONS-IMPLEMENTATION.md`
|
|
||||||
- `/COMMUNITY-STATIONS-FILES.md`
|
|
||||||
|
|
||||||
## File References
|
|
||||||
|
|
||||||
All absolute paths to files:
|
|
||||||
|
|
||||||
### User Features
|
|
||||||
- Desktop Page: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/pages/CommunityStationsPage.tsx`
|
|
||||||
- Mobile Screen: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/mobile/CommunityStationsMobileScreen.tsx`
|
|
||||||
- Submit Form: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/SubmitStationForm.tsx`
|
|
||||||
- Station Card: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/components/CommunityStationCard.tsx`
|
|
||||||
- Hooks: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/hooks/useCommunityStations.ts`
|
|
||||||
|
|
||||||
### Admin Features
|
|
||||||
- Desktop Page: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/pages/AdminCommunityStationsPage.tsx`
|
|
||||||
- Mobile Screen: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/mobile/AdminCommunityStationsMobileScreen.tsx`
|
|
||||||
- Review Card: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/admin/components/CommunityStationReviewCard.tsx`
|
|
||||||
|
|
||||||
### API & Types
|
|
||||||
- API Client: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/api/community-stations.api.ts`
|
|
||||||
- Types: `/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/stations/types/community-stations.types.ts`
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
md = """# MotoVaultPro Homepage Dark Theme Update (Logo-Compatible)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Update the **public marketing homepage** to a **dark theme** so the **new logo (with a black background)** feels native and intentional.
|
|
||||||
|
|
||||||
## Constraints (DO NOT VIOLATE)
|
|
||||||
- **Do not modify the logo image files** (no transparency edits, no recolors).
|
|
||||||
- The logo background **must remain dark/black**.
|
|
||||||
- Keep the existing page structure (nav → hero → welcome → about → features grid → CTA strip → footer).
|
|
||||||
- Only adjust styling/theme + minor hero layout to place the logo cleanly.
|
|
||||||
|
|
||||||
## Source of Truth (what this doc is based on)
|
|
||||||
The current homepage markup is Tailwind-based and includes these notable class patterns:
|
|
||||||
- Root container: `min-h-screen bg-white`
|
|
||||||
- Nav: `bg-white shadow-md sticky top-0`
|
|
||||||
- Section backgrounds: `bg-white`, `bg-gray-100`, `bg-gray-50`
|
|
||||||
- Feature cards: `bg-white p-6` + `text-gray-900` + `text-gray-600`
|
|
||||||
- Footer: `bg-gray-900`
|
|
||||||
- Primary color already matches **Rosso Mugello** (primary-500 = `#7A212A`)
|
|
||||||
|
|
||||||
This doc provides **exact class changes and snippets** to implement in the **React/Tailwind** source (not the built HTML).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
### Logo files (must remain unchanged)
|
|
||||||
Place these in the frontend **public** folder so they can be used without bundler imports:
|
|
||||||
|
|
||||||
- `frontend/public/branding/motovaultpro-title.png`
|
|
||||||
- `frontend/public/branding/motovaultpro-logo-only.png`
|
|
||||||
|
|
||||||
(If your repo structure differs, keep the same `/branding/...` URL path equivalent.)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
- Hero: `/branding/motovaultpro-title.png`
|
|
||||||
- Nav: `/branding/motovaultpro-logo-only.png`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Tokens (from MVP color scheme)
|
|
||||||
Use these colors (hex values) directly in Tailwind arbitrary values or via Tailwind config.
|
|
||||||
- **Nero Daytona**: `#231F1C` (page base)
|
|
||||||
- **Bianco Avus**: `#F2F3F6` (primary text on dark)
|
|
||||||
- **Grigio Titanio**: `#A8B8C0` (secondary text on dark)
|
|
||||||
- **Canna Di Fucile**: `#7E8792` (muted borders/icons)
|
|
||||||
- **Rosso Mugello (Primary)**: `#7A212A` (primary CTA; already `primary-500`)
|
|
||||||
|
|
||||||
### Quick token aliases (recommended)
|
|
||||||
If you can edit `tailwind.config.*`, add:
|
|
||||||
- `nero: "#231F1C"`
|
|
||||||
- `avus: "#F2F3F6"`
|
|
||||||
- `titanio: "#A8B8C0"`
|
|
||||||
- `canna: "#7E8792"`
|
|
||||||
|
|
||||||
If you prefer no config changes, use arbitrary values like `bg-[#231F1C]`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Plan (Step-by-step)
|
|
||||||
|
|
||||||
### 0) Locate the homepage components
|
|
||||||
In the repo, find the marketing homepage by searching for one of these:
|
|
||||||
- `hero-carousel`
|
|
||||||
- `slick-dots`
|
|
||||||
- `MOTOVAULTPRO`
|
|
||||||
- `Thank you for your interest in MotoVaultPro`
|
|
||||||
- `What We Offer`
|
|
||||||
- `min-h-screen bg-white`
|
|
||||||
- `bg-gray-50` + `Our Features`
|
|
||||||
|
|
||||||
Common file names in React projects:
|
|
||||||
- `LandingPage.tsx`, `HomePage.tsx`, `MarketingHome.tsx`, `PublicHome.tsx`
|
|
||||||
- Components: `HeroCarousel.tsx`, `Navbar.tsx`, `FeaturesSection.tsx`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Root container: switch to dark base
|
|
||||||
|
|
||||||
**Before**
|
|
||||||
```tsx
|
|
||||||
<div className="min-h-screen bg-white">
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="min-h-screen bg-[#231F1C] text-[#F2F3F6]">
|
|
||||||
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
This makes all default text inherit Bianco Avus.
|
|
||||||
|
|
||||||
Any sections that used gray text must be updated (see below).
|
|
||||||
|
|
||||||
2) Nav: dark surface + logo-only
|
|
||||||
Nav container
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<nav className="bg-white shadow-md sticky top-0 z-50">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<nav className="sticky top-0 z-50 bg-[#231F1C]/90 backdrop-blur border-b border-white/10">
|
|
||||||
|
|
||||||
Brand area
|
|
||||||
|
|
||||||
Replace the text-only brand with logo-only + text.
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<h1 className="text-2xl font-bold text-primary-500">MotoVaultPro</h1>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<a href="#home" className="flex items-center gap-3">
|
|
||||||
<img
|
|
||||||
src="/branding/motovaultpro-logo-only.png"
|
|
||||||
alt="MotoVaultPro"
|
|
||||||
className="h-8 w-auto"
|
|
||||||
/>
|
|
||||||
<span className="text-lg md:text-xl font-bold tracking-wide text-[#F2F3F6]">
|
|
||||||
MotoVaultPro
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Nav links (desktop)
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<a className="text-gray-700 hover:text-primary-500 transition-colors">Home</a>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<a className="text-white/75 hover:text-white transition-colors">Home</a>
|
|
||||||
|
|
||||||
|
|
||||||
Apply same to Features/About links.
|
|
||||||
|
|
||||||
Mobile menu button
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="text-gray-700 hover:text-primary-500 ...">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="text-white/80 hover:text-white focus:outline-none">
|
|
||||||
|
|
||||||
Nav CTA buttons
|
|
||||||
|
|
||||||
Remove light hover backgrounds (e.g., hover:bg-primary-50) since that becomes bright on dark.
|
|
||||||
|
|
||||||
Sign Up (outline)
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="border-2 border-primary-500 text-primary-500 hover:bg-primary-50 ...">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="border border-primary-500/90 text-primary-500 hover:bg-primary-500/10 hover:border-primary-500 transition-colors duration-300 font-semibold py-2 px-6 rounded-lg">
|
|
||||||
Sign Up
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
Login (filled)
|
|
||||||
Keep as primary, optionally add shadow.
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="bg-primary-500 hover:bg-primary-700 text-white font-semibold py-2 px-6 rounded-lg transition-colors duration-300 shadow-lg shadow-black/30">
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
|
|
||||||
3) Hero: place the full logo on a dark “brand plate”
|
|
||||||
|
|
||||||
Goal: the logo has a black rectangle background; we want it to feel embedded, not pasted.
|
|
||||||
We do that by placing it on a slightly-transparent dark plate with a subtle border and blur.
|
|
||||||
|
|
||||||
Replace hero overlay content
|
|
||||||
|
|
||||||
Find the hero overlay container (currently shows “Welcome to” + big text + Learn More button).
|
|
||||||
|
|
||||||
Before (conceptually)
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<p className="text-white ...">Welcome to</p>
|
|
||||||
<h1 className="text-white ...">MOTOVAULTPRO</h1>
|
|
||||||
<button className="bg-primary-500 ...">Learn More</button>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center px-4">
|
|
||||||
<div className="rounded-2xl border border-white/10 bg-black/40 backdrop-blur-sm p-5 md:p-8 shadow-[0_20px_60px_rgba(0,0,0,0.65)]">
|
|
||||||
<img
|
|
||||||
src="/branding/motovaultpro-title.png"
|
|
||||||
alt="MotoVaultPro — Precision Vehicle Management"
|
|
||||||
className="w-[280px] md:w-[520px] lg:w-[620px] h-auto"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-6 max-w-2xl text-white/85 text-base md:text-lg leading-relaxed">
|
|
||||||
Maintenance, fuel/energy, documents, and reminders — organized per vehicle.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex flex-col sm:flex-row gap-3">
|
|
||||||
<button className="bg-primary-500 hover:bg-primary-700 text-white font-semibold py-3 px-8 rounded-lg transition-colors duration-300 shadow-lg shadow-black/30">
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
<a href="#features" className="inline-flex items-center justify-center rounded-lg border border-white/15 bg-white/5 hover:bg-white/10 text-white font-semibold py-3 px-8 transition-colors duration-300">
|
|
||||||
View Features
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Keep existing hero gradient overlay
|
|
||||||
|
|
||||||
Your hero images already have:
|
|
||||||
bg-gradient-to-b from-black/60 via-black/40 to-black/60
|
|
||||||
Keep it; it improves readability.
|
|
||||||
|
|
||||||
Optional: reduce slick arrow intensity
|
|
||||||
|
|
||||||
If the slick arrows feel too “bright” on dark:
|
|
||||||
|
|
||||||
Reduce opacity and add hover.
|
|
||||||
|
|
||||||
Example CSS:
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
.hero-carousel .slick-prev:before,
|
|
||||||
.hero-carousel .slick-next:before { opacity: 0.45; }
|
|
||||||
.hero-carousel .slick-prev:hover:before,
|
|
||||||
.hero-carousel .slick-next:hover:before { opacity: 0.8; }
|
|
||||||
|
|
||||||
4) Welcome section: dark panel
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 bg-white">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 bg-[#1D1A18] border-t border-white/5">
|
|
||||||
|
|
||||||
|
|
||||||
Text updates (do as class swaps; keep content if you want):
|
|
||||||
|
|
||||||
text-gray-900 → text-[#F2F3F6]
|
|
||||||
|
|
||||||
text-gray-600 → text-[#A8B8C0]
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-[#F2F3F6] mb-6">...</h2>
|
|
||||||
<p className="text-lg text-[#A8B8C0] leading-relaxed mb-8">...</p>
|
|
||||||
|
|
||||||
5) About section: dark base + keep the maroon feature block
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section id="about" className="py-16 px-4 md:px-8 bg-gray-100">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section id="about" className="py-16 px-4 md:px-8 bg-[#231F1C] border-t border-white/5">
|
|
||||||
|
|
||||||
|
|
||||||
Text swaps:
|
|
||||||
|
|
||||||
text-gray-900 → text-[#F2F3F6]
|
|
||||||
|
|
||||||
text-gray-600 → text-[#A8B8C0]
|
|
||||||
|
|
||||||
Trusted Platform block:
|
|
||||||
|
|
||||||
Keep bg-primary-500
|
|
||||||
|
|
||||||
Add subtle border to sharpen against dark background:
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="w-64 h-64 bg-primary-500 rounded-lg border border-white/10 ...">
|
|
||||||
|
|
||||||
6) Features section: dark background + convert white cards to “dark cards”
|
|
||||||
Section background
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 bg-gray-50">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 bg-[#1D1A18] border-t border-white/5">
|
|
||||||
|
|
||||||
Card container (most important change)
|
|
||||||
|
|
||||||
Find the card body container:
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="bg-white p-6">
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-2">...</h3>
|
|
||||||
<p className="text-gray-600 leading-relaxed">...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="bg-white/5 border border-white/10 p-6">
|
|
||||||
<h3 className="text-xl font-bold text-[#F2F3F6] mb-2">...</h3>
|
|
||||||
<p className="text-[#A8B8C0] leading-relaxed">...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Card outer wrapper (shadow + hover)
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300">
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-white/5 shadow-lg shadow-black/30 hover:border-white/20 hover:shadow-xl hover:shadow-black/40 transition-all duration-300">
|
|
||||||
|
|
||||||
|
|
||||||
Note: With the new card body background already set, you may remove redundant bg-white/5 either on wrapper or body.
|
|
||||||
Prefer wrapper bg-white/5 + body bg-transparent, OR wrapper bg-transparent + body bg-white/5. Don’t double-darken unless it looks better.
|
|
||||||
|
|
||||||
7) CTA strip: keep Rosso Mugello but make it richer on dark pages
|
|
||||||
|
|
||||||
Current
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 bg-primary-500 text-white">
|
|
||||||
|
|
||||||
|
|
||||||
Recommended
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<section className="py-16 px-4 md:px-8 text-white bg-[linear-gradient(90deg,#6A1C24_0%,#7A212A_50%,#6A1C24_100%)] border-t border-white/10">
|
|
||||||
|
|
||||||
|
|
||||||
Button:
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="bg-white text-primary-500 hover:bg-gray-100 ...">Get Started</button>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<button className="bg-white/95 text-primary-500 hover:bg-white font-semibold py-3 px-8 rounded-lg transition-colors duration-300 shadow-lg shadow-black/30">
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
|
|
||||||
8) Footer: align to the new dark base
|
|
||||||
|
|
||||||
Before
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<footer className="bg-gray-900 text-white py-8 px-4 md:px-8">
|
|
||||||
<p className="text-gray-400">...</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
|
|
||||||
After
|
|
||||||
|
|
||||||
Always show details
|
|
||||||
<footer className="bg-black text-white py-8 px-4 md:px-8 border-t border-white/10">
|
|
||||||
<p className="text-white/50">© 2025 MotoVaultPro. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
9) Remove or replace light-only Tailwind classes on homepage
|
|
||||||
|
|
||||||
On the homepage only, avoid these patterns because they create “light UI” artifacts on dark pages:
|
|
||||||
|
|
||||||
bg-white, bg-gray-50, bg-gray-100
|
|
||||||
|
|
||||||
text-gray-900, text-gray-700, text-gray-600
|
|
||||||
|
|
||||||
hover:bg-primary-50 (currently a very light blue in your built CSS)
|
|
||||||
|
|
||||||
Use instead:
|
|
||||||
|
|
||||||
dark surfaces: bg-[#231F1C], bg-[#1D1A18], bg-white/5
|
|
||||||
|
|
||||||
text: text-[#F2F3F6], text-[#A8B8C0], text-white/80
|
|
||||||
|
|
||||||
hover: hover:bg-white/10 or hover:bg-primary-500/10
|
|
||||||
|
|
||||||
Acceptance Criteria (QA checklist)
|
|
||||||
|
|
||||||
Homepage background is dark everywhere (no white sections).
|
|
||||||
|
|
||||||
Full logo in hero feels native (sits on a dark “brand plate” with border/blur).
|
|
||||||
|
|
||||||
Nav uses logo-only and remains readable on scroll.
|
|
||||||
|
|
||||||
Feature cards are dark, readable, and keep hover affordances.
|
|
||||||
|
|
||||||
No bright hover states (no primary-50 / light blue flashes).
|
|
||||||
|
|
||||||
Mobile: spacing remains consistent; CTAs are reachable; text contrast is good.
|
|
||||||
|
|
||||||
Suggested PR structure
|
|
||||||
|
|
||||||
feat(ui): dark theme for marketing homepage to match logo background
|
|
||||||
|
|
||||||
Include before/after screenshots at desktop + mobile widths.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
path = Path("/mnt/data/MVP-HOMEPAGE-DARK-THEME-SPEC.md")
|
|
||||||
path.write_text(md, encoding="utf-8")
|
|
||||||
str(path)
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
# Fix New User Signup Wizard Flow
|
|
||||||
|
|
||||||
## Problem Summary
|
|
||||||
The new user signup wizard is broken:
|
|
||||||
1. After Auth0 callback, users go to `/garage` instead of `/verify-email`
|
|
||||||
2. Users can access `/garage/*` without verified email
|
|
||||||
3. Onboarding flow is bypassed entirely
|
|
||||||
4. **New Requirement**: Block login completely at Auth0 for unverified users
|
|
||||||
|
|
||||||
## Root Causes
|
|
||||||
1. **Auth0Provider.tsx:29** - `onRedirectCallback` defaults to `/garage` without checking verification
|
|
||||||
2. **App.tsx:472-481** - Callback route just shows "Processing login..." with no routing logic
|
|
||||||
3. **App.tsx:549+** - Protected routes have no email verification check
|
|
||||||
4. **Auth0** - No rule/action blocking unverified users from logging in
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Auth0 Configuration (Manual Step)
|
|
||||||
|
|
||||||
**Auth0 Dashboard -> Actions -> Flows -> Login**
|
|
||||||
|
|
||||||
Create a Post Login Action to block unverified users:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
exports.onExecutePostLogin = async (event, api) => {
|
|
||||||
if (!event.user.email_verified) {
|
|
||||||
api.access.deny('Please verify your email address before logging in. Check your inbox for a verification link.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures:
|
|
||||||
- Unverified users cannot get a JWT
|
|
||||||
- They see a clear error on the Auth0 login screen
|
|
||||||
- They must click the verification link before logging in
|
|
||||||
|
|
||||||
### Phase 2: Update Signup Flow
|
|
||||||
|
|
||||||
**After signup, redirect to a "Check Your Email" page (not /verify-email)**
|
|
||||||
|
|
||||||
The new flow:
|
|
||||||
1. User submits signup form
|
|
||||||
2. Backend creates Auth0 user (unverified)
|
|
||||||
3. Auth0 automatically sends verification email
|
|
||||||
4. Frontend shows "Check Your Email" page with:
|
|
||||||
- Message: "We've sent a verification link to your email"
|
|
||||||
- Resend button (calls public resend endpoint)
|
|
||||||
- "Back to Login" button
|
|
||||||
5. User clicks email link -> Auth0 marks as verified
|
|
||||||
6. User can now login -> goes to /onboarding
|
|
||||||
|
|
||||||
### Phase 3: Backend Changes
|
|
||||||
|
|
||||||
**File: `backend/src/features/auth/api/auth.routes.ts`**
|
|
||||||
- Add `POST /api/auth/resend-verification-public` (no JWT required)
|
|
||||||
- Takes email address, looks up user, resends verification
|
|
||||||
|
|
||||||
**File: `backend/src/features/auth/api/auth.controller.ts`**
|
|
||||||
- Add `resendVerificationPublic` handler
|
|
||||||
|
|
||||||
**File: `backend/src/features/auth/domain/auth.service.ts`**
|
|
||||||
- Add `resendVerificationByEmail` method
|
|
||||||
|
|
||||||
**File: `backend/src/features/auth/api/auth.routes.ts`**
|
|
||||||
- Add `GET /api/auth/user-status` (JWT required)
|
|
||||||
- Returns: `{ emailVerified, onboardingCompleted, email }`
|
|
||||||
|
|
||||||
**File: `backend/src/core/plugins/auth.plugin.ts`**
|
|
||||||
- Add `/api/auth/user-status` to `VERIFICATION_EXEMPT_ROUTES`
|
|
||||||
|
|
||||||
### Phase 4: Create Callback Handler
|
|
||||||
|
|
||||||
**File: `frontend/src/features/auth/pages/CallbackPage.tsx`** (NEW)
|
|
||||||
- Fetches user status after Auth0 callback
|
|
||||||
- Routes based on status:
|
|
||||||
- Not onboarded -> `/onboarding`
|
|
||||||
- Onboarded -> `/garage` (or returnTo)
|
|
||||||
- Note: Unverified users never reach this (blocked by Auth0)
|
|
||||||
|
|
||||||
**File: `frontend/src/features/auth/mobile/CallbackMobileScreen.tsx`** (NEW)
|
|
||||||
- Mobile version
|
|
||||||
|
|
||||||
### Phase 5: Update Auth0Provider
|
|
||||||
|
|
||||||
**File: `frontend/src/core/auth/Auth0Provider.tsx`**
|
|
||||||
|
|
||||||
Update `onRedirectCallback` (line 27-31):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const onRedirectCallback = (appState?: { returnTo?: string }) => {
|
|
||||||
navigate('/callback', {
|
|
||||||
replace: true,
|
|
||||||
state: { returnTo: appState?.returnTo || '/garage' }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 6: Rename/Update Verify Email Page
|
|
||||||
|
|
||||||
**File: `frontend/src/features/auth/pages/VerifyEmailPage.tsx`**
|
|
||||||
- Rename concept to "Check Your Email" page
|
|
||||||
- Remove polling (user can't be authenticated)
|
|
||||||
- Show static message + resend button (calls public endpoint)
|
|
||||||
- Add "Back to Login" button
|
|
||||||
|
|
||||||
**File: `frontend/src/features/auth/mobile/VerifyEmailMobileScreen.tsx`**
|
|
||||||
- Same changes for mobile
|
|
||||||
|
|
||||||
### Phase 7: Update App.tsx Routing
|
|
||||||
|
|
||||||
**File: `frontend/src/App.tsx`**
|
|
||||||
|
|
||||||
1. Replace callback handling (lines 472-481) with CallbackPage
|
|
||||||
2. Add onboarding guard after authentication check
|
|
||||||
3. Remove email verification check from frontend (Auth0 handles it)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// After isAuthenticated check:
|
|
||||||
// Fetch onboarding status
|
|
||||||
// If not onboarded -> /onboarding
|
|
||||||
// Otherwise -> proceed to /garage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 8: Create Supporting Files
|
|
||||||
|
|
||||||
**File: `frontend/src/core/auth/useUserStatus.ts`** (NEW)
|
|
||||||
- Hook for fetching user status
|
|
||||||
|
|
||||||
**File: `frontend/src/features/auth/api/auth.api.ts`**
|
|
||||||
- Add `getUserStatus()`
|
|
||||||
- Add `resendVerificationPublic(email)` (no auth)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
|
|
||||||
### Auth0 (Manual Configuration)
|
|
||||||
- Create Post Login Action to block unverified users
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- `backend/src/features/auth/api/auth.routes.ts` - Add endpoints
|
|
||||||
- `backend/src/features/auth/api/auth.controller.ts` - Add handlers
|
|
||||||
- `backend/src/features/auth/domain/auth.service.ts` - Add methods
|
|
||||||
- `backend/src/core/plugins/auth.plugin.ts` - Update exempt routes
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- `frontend/src/core/auth/Auth0Provider.tsx` - Fix onRedirectCallback
|
|
||||||
- `frontend/src/App.tsx` - Add route guards and callback handler
|
|
||||||
- `frontend/src/features/auth/pages/CallbackPage.tsx` - NEW
|
|
||||||
- `frontend/src/features/auth/mobile/CallbackMobileScreen.tsx` - NEW
|
|
||||||
- `frontend/src/features/auth/pages/VerifyEmailPage.tsx` - Update to static page
|
|
||||||
- `frontend/src/features/auth/mobile/VerifyEmailMobileScreen.tsx` - Update
|
|
||||||
- `frontend/src/core/auth/useUserStatus.ts` - NEW
|
|
||||||
- `frontend/src/features/auth/api/auth.api.ts` - Add functions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## New User Flow (After Fix)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Signup form submission
|
|
||||||
2. -> POST /api/auth/signup (creates unverified Auth0 user)
|
|
||||||
3. -> Navigate to /verify-email (static "Check Your Email" page)
|
|
||||||
4. User clicks verification link in email
|
|
||||||
5. -> Auth0 marks user as verified
|
|
||||||
6. User clicks "Login" on /verify-email page
|
|
||||||
7. -> Auth0 login succeeds (user is now verified)
|
|
||||||
8. -> /callback page fetches status
|
|
||||||
9. -> Not onboarded? -> /onboarding
|
|
||||||
10. -> Complete onboarding -> /garage
|
|
||||||
```
|
|
||||||
|
|
||||||
## Returning User Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Login attempt (unverified) -> Auth0 blocks with error message
|
|
||||||
2. Login attempt (verified, not onboarded) -> /callback -> /onboarding
|
|
||||||
3. Login attempt (verified, onboarded) -> /callback -> /garage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
- [ ] Auth0 Action blocks unverified login with clear error
|
|
||||||
- [ ] Signup -> check-email page -> verify via email -> login works
|
|
||||||
- [ ] Resend verification from check-email page works
|
|
||||||
- [ ] Verified user (no onboarding) -> onboarding wizard
|
|
||||||
- [ ] Verified + onboarded user -> direct to garage
|
|
||||||
- [ ] Direct URL access to /garage -> requires login
|
|
||||||
- [ ] All flows work on mobile
|
|
||||||
- [ ] All flows work on desktop
|
|
||||||
@@ -1,579 +0,0 @@
|
|||||||
# Security Fixes Implementation Guide
|
|
||||||
|
|
||||||
This document contains all the code changes required to fix the 3 critical security vulnerabilities identified in the audit report.
|
|
||||||
|
|
||||||
**Status**: Ready for implementation
|
|
||||||
**Date**: December 13, 2025
|
|
||||||
**Priority**: P0 (Critical) - Must be implemented before production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fix 1: Insecure Random Number Generation (CRITICAL)
|
|
||||||
|
|
||||||
**CVSS Score**: 8.1 (High)
|
|
||||||
**Location**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Lines**: 321-324
|
|
||||||
|
|
||||||
### Current Code (INSECURE):
|
|
||||||
```typescript
|
|
||||||
function cryptoRandom(): string {
|
|
||||||
// Safe unique suffix for object keys
|
|
||||||
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Changes:
|
|
||||||
|
|
||||||
#### Step 1: Add crypto import
|
|
||||||
**File**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Location**: Line 8 (after existing imports)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import crypto from 'crypto';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Replace cryptoRandom function
|
|
||||||
**File**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Location**: Lines 321-324
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function cryptoRandom(): string {
|
|
||||||
// Cryptographically secure random suffix for object keys
|
|
||||||
return crypto.randomBytes(32).toString('hex');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing:
|
|
||||||
```bash
|
|
||||||
# Rebuild containers
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# Verify document uploads still work
|
|
||||||
# Test that storage keys are now unpredictable
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fix 2: Inadequate File Upload Validation (HIGH)
|
|
||||||
|
|
||||||
**CVSS Score**: 7.5 (High)
|
|
||||||
**Location**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Lines**: 205-216 and entire upload method
|
|
||||||
|
|
||||||
### Required Changes:
|
|
||||||
|
|
||||||
#### Step 1: Add file-type package
|
|
||||||
**File**: `backend/package.json`
|
|
||||||
**Location**: dependencies section
|
|
||||||
|
|
||||||
Add this line to dependencies:
|
|
||||||
```json
|
|
||||||
"file-type": "^19.8.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Add imports
|
|
||||||
**File**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Location**: After line 7 (with other imports)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { fileTypeFromBuffer } from 'file-type';
|
|
||||||
import { Readable } from 'stream';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 3: Replace entire upload method
|
|
||||||
**File**: `backend/src/features/documents/api/documents.controller.ts`
|
|
||||||
**Location**: Lines 175-268 (entire `upload` method)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async upload(request: FastifyRequest<{ Params: IdParams }>, reply: FastifyReply) {
|
|
||||||
const userId = (request as any).user?.sub as string;
|
|
||||||
const documentId = request.params.id;
|
|
||||||
|
|
||||||
logger.info('Document upload requested', {
|
|
||||||
operation: 'documents.upload',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const doc = await this.service.getDocument(userId, documentId);
|
|
||||||
if (!doc) {
|
|
||||||
logger.warn('Document not found for upload', {
|
|
||||||
operation: 'documents.upload.not_found',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
});
|
|
||||||
return reply.code(404).send({ error: 'Not Found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const mp = await (request as any).file({ limits: { files: 1 } });
|
|
||||||
if (!mp) {
|
|
||||||
logger.warn('No file provided for upload', {
|
|
||||||
operation: 'documents.upload.no_file',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
});
|
|
||||||
return reply.code(400).send({ error: 'Bad Request', message: 'No file provided' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define allowed MIME types and their corresponding magic byte signatures
|
|
||||||
const allowedTypes = new Map([
|
|
||||||
['application/pdf', new Set(['application/pdf'])],
|
|
||||||
['image/jpeg', new Set(['image/jpeg'])],
|
|
||||||
['image/png', new Set(['image/png'])],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const contentType = mp.mimetype as string | undefined;
|
|
||||||
if (!contentType || !allowedTypes.has(contentType)) {
|
|
||||||
logger.warn('Unsupported file type for upload (header validation)', {
|
|
||||||
operation: 'documents.upload.unsupported_type',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
content_type: contentType,
|
|
||||||
file_name: mp.filename,
|
|
||||||
});
|
|
||||||
return reply.code(415).send({
|
|
||||||
error: 'Unsupported Media Type',
|
|
||||||
message: 'Only PDF, JPEG, and PNG files are allowed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read first 4100 bytes to detect file type via magic bytes
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
let totalBytes = 0;
|
|
||||||
const targetBytes = 4100;
|
|
||||||
|
|
||||||
for await (const chunk of mp.file) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
totalBytes += chunk.length;
|
|
||||||
if (totalBytes >= targetBytes) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerBuffer = Buffer.concat(chunks);
|
|
||||||
|
|
||||||
// Validate actual file content using magic bytes
|
|
||||||
const detectedType = await fileTypeFromBuffer(headerBuffer);
|
|
||||||
|
|
||||||
if (!detectedType) {
|
|
||||||
logger.warn('Unable to detect file type from content', {
|
|
||||||
operation: 'documents.upload.type_detection_failed',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
content_type: contentType,
|
|
||||||
file_name: mp.filename,
|
|
||||||
});
|
|
||||||
return reply.code(415).send({
|
|
||||||
error: 'Unsupported Media Type',
|
|
||||||
message: 'Unable to verify file type from content'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify detected type matches claimed Content-Type
|
|
||||||
const allowedDetectedTypes = allowedTypes.get(contentType);
|
|
||||||
if (!allowedDetectedTypes || !allowedDetectedTypes.has(detectedType.mime)) {
|
|
||||||
logger.warn('File content does not match Content-Type header', {
|
|
||||||
operation: 'documents.upload.type_mismatch',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
claimed_type: contentType,
|
|
||||||
detected_type: detectedType.mime,
|
|
||||||
file_name: mp.filename,
|
|
||||||
});
|
|
||||||
return reply.code(415).send({
|
|
||||||
error: 'Unsupported Media Type',
|
|
||||||
message: `File content (${detectedType.mime}) does not match claimed type (${contentType})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalName: string = mp.filename || 'upload';
|
|
||||||
const ext = (() => {
|
|
||||||
const e = path.extname(originalName).replace(/^\./, '').toLowerCase();
|
|
||||||
if (e) return e;
|
|
||||||
if (contentType === 'application/pdf') return 'pdf';
|
|
||||||
if (contentType === 'image/jpeg') return 'jpg';
|
|
||||||
if (contentType === 'image/png') return 'png';
|
|
||||||
return 'bin';
|
|
||||||
})();
|
|
||||||
|
|
||||||
class CountingStream extends Transform {
|
|
||||||
public bytes = 0;
|
|
||||||
override _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) {
|
|
||||||
this.bytes += chunk.length || 0;
|
|
||||||
cb(null, chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const counter = new CountingStream();
|
|
||||||
|
|
||||||
// Create a new readable stream from the header buffer + remaining file chunks
|
|
||||||
const headerStream = Readable.from([headerBuffer]);
|
|
||||||
const remainingStream = mp.file;
|
|
||||||
|
|
||||||
// Pipe header first, then remaining content through counter
|
|
||||||
headerStream.pipe(counter, { end: false });
|
|
||||||
headerStream.on('end', () => {
|
|
||||||
remainingStream.pipe(counter);
|
|
||||||
});
|
|
||||||
|
|
||||||
const storage = getStorageService();
|
|
||||||
const bucket = 'documents';
|
|
||||||
const version = 'v1';
|
|
||||||
const unique = cryptoRandom();
|
|
||||||
const key = `documents/${userId}/${doc.vehicle_id}/${doc.id}/${version}/${unique}.${ext}`;
|
|
||||||
|
|
||||||
await storage.putObject(bucket, key, counter, contentType, { 'x-original-filename': originalName });
|
|
||||||
|
|
||||||
const updated = await this.service['repo'].updateStorageMeta(doc.id, userId, {
|
|
||||||
storage_bucket: bucket,
|
|
||||||
storage_key: key,
|
|
||||||
file_name: originalName,
|
|
||||||
content_type: contentType,
|
|
||||||
file_size: counter.bytes,
|
|
||||||
file_hash: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('Document upload completed', {
|
|
||||||
operation: 'documents.upload.success',
|
|
||||||
user_id: userId,
|
|
||||||
document_id: documentId,
|
|
||||||
vehicle_id: doc.vehicle_id,
|
|
||||||
file_name: originalName,
|
|
||||||
content_type: contentType,
|
|
||||||
detected_type: detectedType.mime,
|
|
||||||
file_size: counter.bytes,
|
|
||||||
storage_key: key,
|
|
||||||
});
|
|
||||||
|
|
||||||
return reply.code(200).send(updated);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing:
|
|
||||||
```bash
|
|
||||||
# Install new dependency
|
|
||||||
cd backend && npm install
|
|
||||||
|
|
||||||
# Rebuild containers
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# Test legitimate files
|
|
||||||
# - Upload PDF, JPEG, PNG - should succeed
|
|
||||||
|
|
||||||
# Test attack scenarios
|
|
||||||
# - Rename .exe to .pdf - should be REJECTED
|
|
||||||
# - Spoof Content-Type header - should be REJECTED
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fix 3: Google Maps API Key Exposure (HIGH)
|
|
||||||
|
|
||||||
**CVSS Score**: 6.5 (Medium-High)
|
|
||||||
**Location**: Multiple files in `backend/src/features/stations/`
|
|
||||||
|
|
||||||
### Required Changes:
|
|
||||||
|
|
||||||
#### Backend Changes
|
|
||||||
|
|
||||||
##### Step 1: Update Google Maps Client
|
|
||||||
**File**: `backend/src/features/stations/external/google-maps/google-maps.client.ts`
|
|
||||||
|
|
||||||
**Change 1**: Update `transformPlaceToStation` method (lines 76-95)
|
|
||||||
```typescript
|
|
||||||
private transformPlaceToStation(place: GooglePlace, searchLat: number, searchLng: number): Station {
|
|
||||||
const distance = this.calculateDistance(
|
|
||||||
searchLat,
|
|
||||||
searchLng,
|
|
||||||
place.geometry.location.lat,
|
|
||||||
place.geometry.location.lng
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store photo reference instead of full URL to avoid exposing API key
|
|
||||||
let photoReference: string | undefined;
|
|
||||||
if (place.photos && place.photos.length > 0 && place.photos[0]) {
|
|
||||||
photoReference = place.photos[0].photo_reference;
|
|
||||||
}
|
|
||||||
|
|
||||||
const station: Station = {
|
|
||||||
id: place.place_id,
|
|
||||||
placeId: place.place_id,
|
|
||||||
name: place.name,
|
|
||||||
address: place.vicinity,
|
|
||||||
latitude: place.geometry.location.lat,
|
|
||||||
longitude: place.geometry.location.lng,
|
|
||||||
distance
|
|
||||||
};
|
|
||||||
|
|
||||||
if (photoReference !== undefined) {
|
|
||||||
station.photoReference = photoReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (place.opening_hours?.open_now !== undefined) {
|
|
||||||
station.isOpen = place.opening_hours.open_now;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (place.rating !== undefined) {
|
|
||||||
station.rating = place.rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
return station;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Change 2**: Add new method after `transformPlaceToStation`
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* Fetch photo from Google Maps API using photo reference
|
|
||||||
* Used by photo proxy endpoint to serve photos without exposing API key
|
|
||||||
*/
|
|
||||||
async fetchPhoto(photoReference: string, maxWidth: number = 400): Promise<Buffer> {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${this.baseURL}/photo`,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
photo_reference: photoReference,
|
|
||||||
maxwidth: maxWidth,
|
|
||||||
key: this.apiKey
|
|
||||||
},
|
|
||||||
responseType: 'arraybuffer'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return Buffer.from(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch photo from Google Maps', { error, photoReference });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 2: Update Station Type (Backend)
|
|
||||||
**File**: `backend/src/features/stations/domain/stations.types.ts`
|
|
||||||
**Line**: 20
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// OLD:
|
|
||||||
photoUrl?: string;
|
|
||||||
|
|
||||||
// NEW:
|
|
||||||
photoReference?: string;
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 3: Update Stations Controller
|
|
||||||
**File**: `backend/src/features/stations/api/stations.controller.ts`
|
|
||||||
|
|
||||||
**Add import** at the top:
|
|
||||||
```typescript
|
|
||||||
import { googleMapsClient } from '../external/google-maps/google-maps.client';
|
|
||||||
```
|
|
||||||
|
|
||||||
**Add method** after `removeSavedStation`:
|
|
||||||
```typescript
|
|
||||||
async getStationPhoto(request: FastifyRequest<{ Params: { reference: string } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { reference } = request.params;
|
|
||||||
|
|
||||||
if (!reference) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Photo reference is required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const photoBuffer = await googleMapsClient.fetchPhoto(reference);
|
|
||||||
|
|
||||||
return reply
|
|
||||||
.code(200)
|
|
||||||
.header('Content-Type', 'image/jpeg')
|
|
||||||
.header('Cache-Control', 'public, max-age=86400')
|
|
||||||
.send(photoBuffer);
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error('Error fetching station photo', {
|
|
||||||
error,
|
|
||||||
reference: request.params.reference
|
|
||||||
});
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to fetch station photo'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 4: Update Stations Routes
|
|
||||||
**File**: `backend/src/features/stations/api/stations.routes.ts`
|
|
||||||
|
|
||||||
**Add route** before the closing `};`:
|
|
||||||
```typescript
|
|
||||||
// GET /api/stations/photo/:reference - Proxy for Google Maps photos
|
|
||||||
fastify.get<{ Params: { reference: string } }>('/stations/photo/:reference', {
|
|
||||||
preHandler: [fastify.authenticate],
|
|
||||||
handler: stationsController.getStationPhoto.bind(stationsController)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 5: Update Stations Service
|
|
||||||
**File**: `backend/src/features/stations/domain/stations.service.ts`
|
|
||||||
**Line**: 144
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// OLD:
|
|
||||||
photoUrl: station?.photoUrl,
|
|
||||||
|
|
||||||
// NEW:
|
|
||||||
photoReference: station?.photoReference,
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Frontend Changes
|
|
||||||
|
|
||||||
##### Step 6: Update Station Type (Frontend)
|
|
||||||
**File**: `frontend/src/features/stations/types/stations.types.ts`
|
|
||||||
**Line**: 60
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// OLD:
|
|
||||||
photoUrl?: string;
|
|
||||||
|
|
||||||
// NEW:
|
|
||||||
photoReference?: string;
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 7: Create Photo Utils Helper
|
|
||||||
**File**: `frontend/src/features/stations/utils/photo-utils.ts` (NEW FILE)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Helper utilities for station photos
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate secure photo URL using backend proxy endpoint
|
|
||||||
* This prevents exposing the Google Maps API key to the frontend
|
|
||||||
*/
|
|
||||||
export function getStationPhotoUrl(photoReference: string | undefined): string | undefined {
|
|
||||||
if (!photoReference) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/api/stations/photo/${encodeURIComponent(photoReference)}`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Step 8: Update StationCard Component
|
|
||||||
**File**: `frontend/src/features/stations/components/StationCard.tsx`
|
|
||||||
|
|
||||||
**Add import** at the top:
|
|
||||||
```typescript
|
|
||||||
import { getStationPhotoUrl } from '../utils/photo-utils';
|
|
||||||
```
|
|
||||||
|
|
||||||
**Update photo rendering** (lines 86-93):
|
|
||||||
```typescript
|
|
||||||
// OLD:
|
|
||||||
{station.photoUrl && (
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
height="200"
|
|
||||||
image={station.photoUrl}
|
|
||||||
alt={station.name}
|
|
||||||
sx={{ objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
// NEW:
|
|
||||||
{station.photoReference && (
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
height="200"
|
|
||||||
image={getStationPhotoUrl(station.photoReference)}
|
|
||||||
alt={station.name}
|
|
||||||
sx={{ objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing:
|
|
||||||
```bash
|
|
||||||
# Rebuild containers
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# Test station search with photos
|
|
||||||
# Verify API key is NOT in network requests
|
|
||||||
# Verify photos still load correctly
|
|
||||||
# Test on mobile and desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Checklist
|
|
||||||
|
|
||||||
- [ ] Apply Fix 1: Insecure Random Generation
|
|
||||||
- [ ] Add crypto import
|
|
||||||
- [ ] Replace cryptoRandom function
|
|
||||||
- [ ] Test document uploads
|
|
||||||
|
|
||||||
- [ ] Apply Fix 2: File Upload Validation
|
|
||||||
- [ ] Add file-type to package.json
|
|
||||||
- [ ] Run npm install
|
|
||||||
- [ ] Add imports
|
|
||||||
- [ ] Replace upload method
|
|
||||||
- [ ] Test with legitimate files
|
|
||||||
- [ ] Test with malicious files
|
|
||||||
|
|
||||||
- [ ] Apply Fix 3: API Key Exposure
|
|
||||||
- [ ] Update Google Maps client
|
|
||||||
- [ ] Update backend Station type
|
|
||||||
- [ ] Add controller method
|
|
||||||
- [ ] Add route
|
|
||||||
- [ ] Update service
|
|
||||||
- [ ] Update frontend Station type
|
|
||||||
- [ ] Create photo-utils.ts
|
|
||||||
- [ ] Update StationCard component
|
|
||||||
- [ ] Test photos load correctly
|
|
||||||
- [ ] Verify API key not in network
|
|
||||||
|
|
||||||
- [ ] Run all tests
|
|
||||||
- [ ] Backend tests: `cd backend && npm test`
|
|
||||||
- [ ] Frontend tests: `cd frontend && npm test`
|
|
||||||
|
|
||||||
- [ ] Rebuild and verify
|
|
||||||
- [ ] `make rebuild`
|
|
||||||
- [ ] Manual testing of all features
|
|
||||||
- [ ] Security verification (no API keys exposed)
|
|
||||||
|
|
||||||
- [ ] Create git commit with security fixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Verification
|
|
||||||
|
|
||||||
After implementing all fixes, verify:
|
|
||||||
|
|
||||||
1. **Document Storage Keys**: Now using `crypto.randomBytes()` - unpredictable
|
|
||||||
2. **File Upload**: Magic byte validation rejects spoofed files
|
|
||||||
3. **Google Maps API**: Key never sent to frontend, all requests proxied
|
|
||||||
|
|
||||||
Run security scan:
|
|
||||||
```bash
|
|
||||||
# Check for hardcoded secrets
|
|
||||||
grep -r "AIza" backend/ frontend/
|
|
||||||
# Should return no results
|
|
||||||
|
|
||||||
# Verify crypto usage
|
|
||||||
grep -r "Math.random()" backend/
|
|
||||||
# Should return no results in production code
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Post-Implementation
|
|
||||||
|
|
||||||
After all fixes are applied and tested:
|
|
||||||
|
|
||||||
1. Update AUDIT.md to reflect fixed status
|
|
||||||
2. Commit changes with security fix message
|
|
||||||
3. Run full test suite
|
|
||||||
4. Deploy to staging for verification
|
|
||||||
5. Schedule production deployment
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,134 +0,0 @@
|
|||||||
# Admin Settings Frontend Implementation Plan
|
|
||||||
|
|
||||||
## Audience & Scope
|
|
||||||
- **Intended executor**: AI agent implementing MotoVaultPro admin settings UI across desktop and mobile.
|
|
||||||
- **Scope**: Frontend-only tasks within `frontend/`, coordinating with existing backend admin APIs. Includes real-time audit log integration and bulk operations across admin users, catalog entities, and station management.
|
|
||||||
|
|
||||||
## Current State Summary
|
|
||||||
- Routes exist (`frontend/src/pages/admin/*.tsx`, `frontend/src/features/admin/mobile/*.tsx`) but contain placeholder copy.
|
|
||||||
- Hooks and API clients (`frontend/src/features/admin/hooks/*`, `frontend/src/features/admin/api/admin.api.ts`) already wrap CRUD endpoints but lack bulk helpers and streaming.
|
|
||||||
- Settings pages link into admin routes; `useAdminAccess` gate is wired.
|
|
||||||
- No shared admin layout, tables, or selection utilities; no real-time audit consumption; no bulk UI.
|
|
||||||
|
|
||||||
## Key Requirements
|
|
||||||
1. **Real-time audit logging** for admin operations (desktop + mobile).
|
|
||||||
2. **Bulk operations**: multi-select + batch mutate/delete/revoke across admin users, catalog hierarchy, stations.
|
|
||||||
3. **Desktop / Mobile parity** while respecting CLAUDE.md mobile + desktop mandate and existing design system.
|
|
||||||
|
|
||||||
Assumptions:
|
|
||||||
- Backend will expose streaming endpoint (`/api/admin/audit-logs/stream`) using SSE. (If absent, coordinate for addition.)
|
|
||||||
- Backend will provide/extend batch mutation endpoints or accept arrays in current ones.
|
|
||||||
- No additional design assets; follow existing Material UI / GlassCard patterns.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 0 – Prep & Validation
|
|
||||||
- Confirm backend endpoints:
|
|
||||||
- `GET /api/admin/audit-logs/stream` (SSE) payload schema.
|
|
||||||
- Batch endpoints for admins (`POST /admin/admins/bulk`, `PATCH /admin/admins/bulk-revoke`, etc.), catalog (`/admin/catalog/{entity}/bulk-delete`), stations (`/admin/stations/bulk-delete`).
|
|
||||||
- Response format + error contracts.
|
|
||||||
- Document agreements in `docs/ADMIN.md` and update API client typings before UI work.
|
|
||||||
|
|
||||||
### Phase 1 – Shared Infrastructure
|
|
||||||
- Add shared admin components under `frontend/src/features/admin/components/`:
|
|
||||||
- `AdminSectionHeader`
|
|
||||||
- `AdminDataGrid` (wrapper around MUI DataGrid or Table) with checkbox selection and toolbar slot.
|
|
||||||
- `SelectionToolbar` + `BulkActionDialog`.
|
|
||||||
- `AuditLogPanel` (desktop) and `AuditLogDrawer` (mobile).
|
|
||||||
- `EmptyState`, `ErrorState`, `Skeleton` variants.
|
|
||||||
- Utility hooks/services:
|
|
||||||
- `useBulkSelection` (manages item selection, select all, reset).
|
|
||||||
- `useAuditLogStream` (SSE handling, merge into cache, pause/resume).
|
|
||||||
- `useAdminRealtimeEffect` (common real-time logic for both platforms).
|
|
||||||
- Error normalization helper for API responses.
|
|
||||||
- Update `admin.api.ts` to include bulk endpoints and streaming subscription helper.
|
|
||||||
- Ensure types in `admin.types.ts` cover new request/response payloads.
|
|
||||||
|
|
||||||
### Phase 2 – Admin Users Experience
|
|
||||||
- **Desktop (`AdminUsersPage.tsx`)**:
|
|
||||||
- Replace placeholder with layout:
|
|
||||||
- Header (stats summary cards).
|
|
||||||
- `AdminDataGrid` listing admins (columns: email, role, status, created/updated, last activity).
|
|
||||||
- Toolbar actions: Invite, Revoke, Reinstate, Delete (single + bulk), export CSV placeholder.
|
|
||||||
- Inline filters/search.
|
|
||||||
- Audit log side panel fed by `useAuditLogStream`.
|
|
||||||
- Modals/forms:
|
|
||||||
- Invite admin (react-hook-form + Zod validation).
|
|
||||||
- Confirm dialogs for revoke/reinstate/delete (bulk friendly).
|
|
||||||
- State management:
|
|
||||||
- Use React Query hooks (`useAdmins`, new `useBulkRevokeAdmins`, etc.).
|
|
||||||
- Optimistic updates where safe; fallback to refetch on failure.
|
|
||||||
- Surface backend constraints (last admin protection) in toasts/dialogs.
|
|
||||||
- **Mobile (`AdminUsersMobileScreen.tsx`)**:
|
|
||||||
- Card-based list with segmented controls.
|
|
||||||
- Multi-select mode triggered by long-press or “Select” button; sticky bottom action bar for bulk operations.
|
|
||||||
- Slide-in drawer for audit log stream; allow collapse to preserve screen space.
|
|
||||||
- Ensure loading/error/empty states match mobile pattern.
|
|
||||||
|
|
||||||
### Phase 3 – Vehicle Catalog Management
|
|
||||||
- Extend API hooks for per-entity bulk operations (`useDeleteMakesBulk`, etc.) and streaming updates.
|
|
||||||
- **Desktop (`AdminCatalogPage.tsx`)**:
|
|
||||||
- Two-column layout: left panel shows hierarchical tree (Makes → Models → Years → Trims → Engines). Right panel shows detail grid for selected level.
|
|
||||||
- Support multi-select in each grid with bulk delete; confirm cascading impacts (warn when deleting parents).
|
|
||||||
- Modals for create/edit per entity using shared form component (with validation & parent context).
|
|
||||||
- Audit log panel filtered to catalog-related actions.
|
|
||||||
- Show breadcrumbs + context metadata (created/updated timestamps).
|
|
||||||
- **Mobile (`AdminCatalogMobileScreen.tsx`)**:
|
|
||||||
- Drill-down navigation (list of makes → models → ...).
|
|
||||||
- Selection mode toggles for bulk delete at current depth; use bottom sheet to display actions.
|
|
||||||
- Provide “Recent Changes” sheet consuming audit stream (filtered).
|
|
||||||
- Handle cache invalidation across hierarchies (e.g., deleting a make invalidates models/years/trims queries). Consider using queryClient `invalidateQueries` with partial keys.
|
|
||||||
|
|
||||||
### Phase 4 – Station Oversight
|
|
||||||
- Hook updates: add `useBulkDeleteStations`, `useBulkRestoreStations` if available, with optional `force` flag.
|
|
||||||
- **Desktop (`AdminStationsPage.tsx`)**:
|
|
||||||
- Data grid with columns (name, address, status, last modified, createdBy). Add search bar and filter chips (active, soft-deleted).
|
|
||||||
- Bulk selection with delete (soft/hard toggle), restore, export stub.
|
|
||||||
- Station detail drawer with metadata and quick actions.
|
|
||||||
- Audit log panel focusing on station events; highlight critical operations via toast (e.g., hard deletes).
|
|
||||||
- **Mobile (`AdminStationsMobileScreen.tsx`)**:
|
|
||||||
- Card list with quick actions (edit, delete, restore). Multi-select mode with sticky action bar.
|
|
||||||
- Provide filter tabs (All / Active / Deleted).
|
|
||||||
- Integrate audit log bottom sheet.
|
|
||||||
|
|
||||||
### Phase 5 – Integration & Routing Enhancements
|
|
||||||
- Introduce route wrapper/components (e.g., `AdminUsersRoute`) that detect viewport using `useMediaQuery` and render desktop or mobile variant; ensures shared logic and prevents duplicate routing code.
|
|
||||||
- Update navigation flows, ensuring mobile bottom navigation can reach admin sections gracefully.
|
|
||||||
- Document keyboard shortcuts or focus management for accessibility (bulk selection, audit log toggles).
|
|
||||||
|
|
||||||
### Phase 6 – Testing & QA
|
|
||||||
- Add unit tests for new hooks (`useAuditLogStream`, bulk hooks) using Jest + Testing Library. Mock EventSource for streaming tests.
|
|
||||||
- Component tests:
|
|
||||||
- Desktop grids: selection toggles, bulk action dialogs, form validation.
|
|
||||||
- Mobile screens: selection mode toggling, action bar behaviors.
|
|
||||||
- Audit log panels: streaming update rendering, pause/resume controls.
|
|
||||||
- Visual regression smoke tests if tooling available; otherwise document manual screenshot checkpoints.
|
|
||||||
- Manual QA matrix:
|
|
||||||
- Desktop ≥1280px and mobile ≤480px.
|
|
||||||
- Test flows: invite admin, revoke/reinstate, bulk revoke, catalog cascading delete, station soft/hard delete, audit log live updates.
|
|
||||||
|
|
||||||
## Deliverables Checklist
|
|
||||||
- [ ] Updated API client + types for batch + streaming.
|
|
||||||
- [ ] Shared admin UI components & utilities.
|
|
||||||
- [ ] Desktop admin pages fully functional with bulk + real-time features.
|
|
||||||
- [ ] Mobile admin screens matching functionality.
|
|
||||||
- [ ] Comprehensive tests covering new flows.
|
|
||||||
- [ ] Documentation updates (API usage, manual QA steps).
|
|
||||||
|
|
||||||
## Risks & Mitigations
|
|
||||||
- **Streaming availability**: If backend stream not ready, fall back to polling with progressive enhancement; keep SSE integration behind feature flag.
|
|
||||||
- **Bulk API inconsistencies**: Align payload format with backend; add defensive UI (disable actions until backend confirms support).
|
|
||||||
- **State synchronization**: Ensure query invalidation covers dependent entities; consider structured query keys and `queryClient.setQueryData` for incremental updates.
|
|
||||||
- **Mobile UX complexity**: Prototype selection mode early to validate ergonomics; leverage bottom sheets to avoid cramped toolbars.
|
|
||||||
|
|
||||||
## Follow-up Questions (Resolved)
|
|
||||||
1. Real-time audit logs required — implement SSE-based stream handling.
|
|
||||||
2. Bulk operations mandatory — support multi-select + batch actions across admin users, catalog entities, stations.
|
|
||||||
3. No additional design constraints — rely on existing Material UI and GlassCard paradigms.
|
|
||||||
|
|
||||||
## Handoff Notes
|
|
||||||
- Keep code comments concise per developer guidelines; avoid introducing new design systems.
|
|
||||||
- Validate hooks for Auth0 dependency (ensure disabled when unauthenticated).
|
|
||||||
- Coordinate with backend team if API gaps found; document interim shims.
|
|
||||||
- Maintain responsiveness and accessibility; ensure touch targets ≥44px and keyboard operability on desktop grids.
|
|
||||||
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
# Agent 7: Testing & Validation - Final Report
|
|
||||||
|
|
||||||
**Date**: 2025-11-11
|
|
||||||
**Status**: COMPREHENSIVE VALIDATION COMPLETE
|
|
||||||
**Migration Status**: READY FOR PRODUCTION (with minor data quality note)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The vehicle dropdown migration from ID-based to string-based architecture has been successfully implemented and validated. All critical tests pass. A minor data quality issue affecting 0.04% of records (452 of 1.1M+) was identified in the source data but does not impact functionality.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Results Summary
|
|
||||||
|
|
||||||
### 1. DATABASE VALIDATION
|
|
||||||
|
|
||||||
#### 1.1 Record Counts
|
|
||||||
- **Engines**: 30,066 ✓ PASS
|
|
||||||
- **Transmissions**: 828 ✓ PASS
|
|
||||||
- **Vehicle Options**: 1,122,644 ✓ PASS
|
|
||||||
|
|
||||||
#### 1.2 Data Quality
|
|
||||||
- **Year Range**: 1980-2026 (47 distinct years) ✓ PASS
|
|
||||||
- **Makes Count**: 53 manufacturers ✓ PASS
|
|
||||||
- **Title Case Validation**: All makes properly formatted (e.g., "Acura", "Alfa Romeo", "Aston Martin") ✓ PASS
|
|
||||||
- **NULL Engine Handling**: 12,005 records (1.07%) - Electric vehicles handled correctly ✓ PASS
|
|
||||||
- **Data Corruption**: 452 records (0.04%) with HTML entities in model names - Source data issue, non-blocking
|
|
||||||
|
|
||||||
#### 1.3 Database Functions
|
|
||||||
- `get_makes_for_year()` - ✓ WORKING
|
|
||||||
- `get_models_for_year_make()` - ✓ WORKING
|
|
||||||
- `get_trims_for_year_make_model()` - ✓ WORKING
|
|
||||||
- `get_options_for_vehicle()` - ✓ WORKING (via query)
|
|
||||||
|
|
||||||
#### 1.4 Query Performance
|
|
||||||
- **Makes query**: 1.5ms (target <50ms) ✓ EXCELLENT
|
|
||||||
- **Models query**: Expected <10ms based on index usage ✓ EXCELLENT
|
|
||||||
- **All queries use indexes**: idx_vehicle_year_make, idx_vehicle_year_make_model ✓ PASS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. BACKEND API VALIDATION
|
|
||||||
|
|
||||||
#### 2.1 Endpoints Status
|
|
||||||
- **GET /api/vehicles/dropdown/years** - ✓ IMPLEMENTED (Returns 401 without auth - expected)
|
|
||||||
- **GET /api/vehicles/dropdown/makes** - ✓ IMPLEMENTED
|
|
||||||
- **GET /api/vehicles/dropdown/models** - ✓ IMPLEMENTED
|
|
||||||
- **GET /api/vehicles/dropdown/trims** - ✓ IMPLEMENTED
|
|
||||||
- **GET /api/vehicles/dropdown/engines** - ✓ IMPLEMENTED
|
|
||||||
- **GET /api/vehicles/dropdown/transmissions** - ✓ IMPLEMENTED
|
|
||||||
|
|
||||||
#### 2.2 Parameter Validation
|
|
||||||
- String parameters (make, model, trim): ✓ CONFIRMED
|
|
||||||
- Proper error handling for missing parameters: ✓ CONFIRMED
|
|
||||||
- Special character handling (e.g., "Land Rover", "Mercedes-Benz"): ✓ CONFIRMED
|
|
||||||
|
|
||||||
#### 2.3 Response Format
|
|
||||||
- All endpoints return `string[]` format: ✓ CONFIRMED
|
|
||||||
- Transmissions return real data (not hardcoded): ✓ CONFIRMED
|
|
||||||
- Electric vehicles return "N/A (Electric)" for engines: ✓ CONFIRMED
|
|
||||||
|
|
||||||
#### 2.4 Service Layer
|
|
||||||
- VehiclesService correctly delegates to VehicleDataService: ✓ CONFIRMED
|
|
||||||
- VehicleDataService uses VehicleDataRepository: ✓ CONFIRMED
|
|
||||||
- Caching implemented via PlatformCacheService: ✓ CONFIRMED
|
|
||||||
|
|
||||||
#### 2.5 Repository Layer
|
|
||||||
- All queries use parameterized statements (no SQL injection risk): ✓ CONFIRMED
|
|
||||||
- Database functions properly invoked: ✓ CONFIRMED
|
|
||||||
- NULL engine handling for electric vehicles: ✓ CONFIRMED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. CONTAINER HEALTH
|
|
||||||
|
|
||||||
#### 3.1 Running Services
|
|
||||||
- **mvp-traefik**: ✓ HEALTHY (up 4 days)
|
|
||||||
- **mvp-backend**: ✓ HEALTHY (up 3 days, responding to health checks)
|
|
||||||
- **mvp-frontend**: ✓ HEALTHY (up 3 days, serving static content)
|
|
||||||
- **mvp-postgres**: ✓ HEALTHY (up 4 days, all queries working)
|
|
||||||
- **mvp-redis**: ✓ HEALTHY (up 4 days)
|
|
||||||
|
|
||||||
#### 3.2 Health Endpoints
|
|
||||||
- Backend `/health`: ✓ 200 OK (continuous health checks in logs)
|
|
||||||
- Frontend `/`: ✓ 200 OK (serving index.html)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. CODE STRUCTURE VALIDATION
|
|
||||||
|
|
||||||
#### 4.1 Architecture Compliance
|
|
||||||
- **Feature Capsule Pattern**: ✓ CONFIRMED
|
|
||||||
- Feature isolated in `backend/src/features/vehicles/`
|
|
||||||
- Proper separation: domain, api, data layers
|
|
||||||
|
|
||||||
- **Platform Integration**: ✓ CONFIRMED
|
|
||||||
- Platform feature properly integrated
|
|
||||||
- VehicleDataService correctly used by VehiclesService
|
|
||||||
- VIN decode service available
|
|
||||||
|
|
||||||
#### 4.2 Type Safety
|
|
||||||
- **TypeScript Usage**: ✓ CONFIRMED
|
|
||||||
- Proper interfaces: CreateVehicleBody, UpdateVehicleBody
|
|
||||||
- Route handlers properly typed with FastifyRequest/FastifyReply
|
|
||||||
- Repository and Service layers fully typed
|
|
||||||
|
|
||||||
#### 4.3 Error Handling
|
|
||||||
- **Controller Layer**: ✓ CONFIRMED
|
|
||||||
- Try/catch blocks in all handlers
|
|
||||||
- Proper HTTP status codes (400, 401, 404, 500)
|
|
||||||
- Error logging via logger service
|
|
||||||
|
|
||||||
#### 4.4 Authentication
|
|
||||||
- **Fastify JWT**: ✓ CONFIRMED
|
|
||||||
- Prehandler authentication on all protected routes
|
|
||||||
- `fastify.authenticate` middleware implemented
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. DATABASE MIGRATIONS
|
|
||||||
|
|
||||||
#### 5.1 Schema
|
|
||||||
- **vehicles_options table**: 1,122,644 records ✓ EXISTS
|
|
||||||
- **engines table**: 30,066 records ✓ EXISTS
|
|
||||||
- **transmissions table**: 828 records ✓ EXISTS
|
|
||||||
- **Indexes created**: idx_vehicle_year_make, idx_vehicle_year_make_model ✓ EXISTS
|
|
||||||
|
|
||||||
#### 5.2 Data Integrity
|
|
||||||
- **No orphaned records**: All engine_id and transmission_id references valid ✓ CONFIRMED
|
|
||||||
- **Cascade relationships**: Properly maintained ✓ CONFIRMED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
|
|
||||||
### Database Tests
|
|
||||||
- ✓ Record count validation
|
|
||||||
- ✓ Data quality checks (year range, title case, NULL handling)
|
|
||||||
- ✓ Database function testing
|
|
||||||
- ✓ Query performance testing
|
|
||||||
- ✓ Index usage validation
|
|
||||||
|
|
||||||
### Backend Tests
|
|
||||||
- ✓ Endpoint accessibility
|
|
||||||
- ✓ Parameter validation
|
|
||||||
- ✓ Response format verification
|
|
||||||
- ✓ Error handling
|
|
||||||
- ✓ Authentication requirement
|
|
||||||
- ✓ Service layer integration
|
|
||||||
|
|
||||||
### Frontend Tests
|
|
||||||
- ✓ Container health
|
|
||||||
- ✓ Static content serving
|
|
||||||
- ✓ HTTPS/Traefik routing
|
|
||||||
|
|
||||||
### Performance Tests
|
|
||||||
- ✓ Query execution times
|
|
||||||
- ✓ Container health checks (continuous 30s intervals)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues & Limitations
|
|
||||||
|
|
||||||
### 1. Minor Data Quality Issue
|
|
||||||
**Severity**: LOW
|
|
||||||
**Description**: 452 records (0.04%) contain HTML entity encoding in model names
|
|
||||||
**Example**: "Ford Kuga Photos, engines & full specs"
|
|
||||||
**Root Cause**: Source vehicle database contains this data
|
|
||||||
**Impact**: Minimal - affects <0.05% of records
|
|
||||||
**User Experience**: Users selecting vehicles would see malformed model names for affected makes/models
|
|
||||||
**Recommendation**: Consider data cleaning in next ETL run (non-blocking for current release)
|
|
||||||
|
|
||||||
### 2. Limited Test Execution Environment
|
|
||||||
**Note**: Production Docker containers don't include dev dependencies (jest, eslint, tsc)
|
|
||||||
**Workaround**: This is correct for production. Testing requires separate dev environment
|
|
||||||
**Resolution**: Use `make shell-backend` in development or rebuild with dev dependencies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
### Database ✓
|
|
||||||
- [x] All tables created
|
|
||||||
- [x] Record counts verified (30K engines, 828 transmissions, 1.1M+ vehicles)
|
|
||||||
- [x] Data quality validated (Title Case, year range, NULL handling)
|
|
||||||
- [x] Database functions operational
|
|
||||||
- [x] Query performance < 50ms
|
|
||||||
- [x] Indexes created and being used
|
|
||||||
|
|
||||||
### Backend ✓
|
|
||||||
- [x] All dropdown endpoints implemented
|
|
||||||
- [x] String parameters working correctly
|
|
||||||
- [x] String array responses working correctly
|
|
||||||
- [x] Transmissions return real data (not hardcoded)
|
|
||||||
- [x] Error handling functional
|
|
||||||
- [x] Authentication required
|
|
||||||
- [x] Service and repository layers integrated
|
|
||||||
|
|
||||||
### Frontend ✓
|
|
||||||
- [x] Container running and healthy
|
|
||||||
- [x] Serving static content (HTTP 200)
|
|
||||||
- [x] Configuration loaded (Google Maps API key)
|
|
||||||
- [x] Nginx properly configured
|
|
||||||
|
|
||||||
### Integration ✓
|
|
||||||
- [x] Backend communicating with database
|
|
||||||
- [x] All 5 containers healthy
|
|
||||||
- [x] Traefik routing operational
|
|
||||||
- [x] PostgreSQL responding to queries
|
|
||||||
- [x] Redis cache available
|
|
||||||
|
|
||||||
### Performance ✓
|
|
||||||
- [x] Database queries < 50ms
|
|
||||||
- [x] Container health checks passing
|
|
||||||
- [x] No memory issues detected in logs
|
|
||||||
- [x] Continuous health monitoring active
|
|
||||||
|
|
||||||
### Security ✓
|
|
||||||
- [x] SQL injection prevention (parameterized queries)
|
|
||||||
- [x] Authentication enforced on API endpoints
|
|
||||||
- [x] Authorization checks in place
|
|
||||||
- [x] Error messages don't leak sensitive info
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Manual Testing Notes
|
|
||||||
|
|
||||||
### Frontend End-to-End Testing
|
|
||||||
The frontend can be tested by:
|
|
||||||
1. Navigating to the application (via Traefik at https://admin.motovaultpro.com)
|
|
||||||
2. Logging in with Auth0 credentials
|
|
||||||
3. Creating a new vehicle:
|
|
||||||
- Select year → Make → Model → Trim → Engine/Transmission should cascade correctly
|
|
||||||
- The dropdowns will now use the new string-based API
|
|
||||||
- String values should be displayed correctly in the form and in saved vehicles
|
|
||||||
|
|
||||||
4. Editing an existing vehicle:
|
|
||||||
- All dropdowns should pre-populate with the currently selected string values
|
|
||||||
- Changing upstream values (year, make, model) should cascade correctly
|
|
||||||
|
|
||||||
5. Testing electric vehicles (Tesla):
|
|
||||||
- Tesla vehicles in the 2024 year range should show "N/A (Electric)" for engines
|
|
||||||
- Transmission selection should still work
|
|
||||||
|
|
||||||
### Mobile Testing
|
|
||||||
- Ensure forms are responsive on mobile viewport (< 768px width)
|
|
||||||
- Test touch interactions with dropdown menus
|
|
||||||
- Verify no horizontal scrolling occurs
|
|
||||||
- Confirm form submission works on mobile
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations for Next Steps
|
|
||||||
|
|
||||||
### Before Production Deployment
|
|
||||||
1. **Optional**: Schedule data cleaning job to fix HTML entities in 0.04% of records
|
|
||||||
2. **Recommended**: Set up continuous monitoring of database queries
|
|
||||||
3. **Recommended**: Configure alerts for query performance degradation
|
|
||||||
|
|
||||||
### For Next Sprint
|
|
||||||
1. Add integration tests for new dropdown cascade behavior
|
|
||||||
2. Add frontend component tests for dropdown interaction
|
|
||||||
3. Consider caching strategy review (currently 5 minutes for most data)
|
|
||||||
4. Performance baseline recording for future comparisons
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Completion Status
|
|
||||||
|
|
||||||
**Overall Status**: ✅ READY FOR PRODUCTION
|
|
||||||
|
|
||||||
All quality gates passed:
|
|
||||||
- ✅ Database: 1.1M+ records migrated successfully
|
|
||||||
- ✅ API: All endpoints implemented and tested
|
|
||||||
- ✅ Backend: Service and repository layers complete
|
|
||||||
- ✅ Frontend: Container running and healthy
|
|
||||||
- ✅ Performance: All queries < 50ms
|
|
||||||
- ✅ Security: All checks passing
|
|
||||||
- ✅ Error Handling: Comprehensive coverage
|
|
||||||
- ✅ Data Quality: 99.96% clean data (0.04% corruption in source)
|
|
||||||
|
|
||||||
**Issues Found**: 1 minor (non-blocking)
|
|
||||||
- 452 records with HTML entity corruption (0.04% of total)
|
|
||||||
|
|
||||||
**Sign-Off**: This migration is approved for production deployment.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Completed By**: Agent 7 (Testing & Validation)
|
|
||||||
**Completion Date**: 2025-11-11
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
# Agent 7: Validation Test Commands Reference
|
|
||||||
|
|
||||||
**Date**: 2025-11-11
|
|
||||||
**Purpose**: Document all validation test commands executed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Record Count Verification
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT (SELECT COUNT(*) FROM engines) as engines, \
|
|
||||||
(SELECT COUNT(*) FROM transmissions) as transmissions, \
|
|
||||||
(SELECT COUNT(*) FROM vehicle_options) as vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: engines: 30066, transmissions: 828, vehicle_options: 1122644
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Year Range Validation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT MIN(year) as min_year, MAX(year) as max_year, \
|
|
||||||
COUNT(DISTINCT year) as year_count \
|
|
||||||
FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: min_year: 1980, max_year: 2026, year_count: 47
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Distinct Makes Count
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(DISTINCT make) as distinct_makes \
|
|
||||||
FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: distinct_makes: 53
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Title Case Validation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT DISTINCT make FROM vehicle_options ORDER BY make LIMIT 10;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Acura, Alfa Romeo, Aston Martin, Audi, BMW, Bentley, Buick, Cadillac, Chevrolet, Chrysler
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NULL Engine Handling
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) as total, \
|
|
||||||
COUNT(*) FILTER (WHERE engine_id IS NULL) as null_engines, \
|
|
||||||
ROUND(100.0 * COUNT(*) FILTER (WHERE engine_id IS NULL) / COUNT(*), 2) as percentage \
|
|
||||||
FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: total: 1122644, null_engines: 12005, percentage: 1.07
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Function: get_makes_for_year()
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_makes_for_year(2024) LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Returns make names (Acura, Aston Martin, Audi, BMW, Buick)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Function: get_models_for_year_make()
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_models_for_year_make(2024, 'Ford') LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Returns model names with some data quality issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Function: get_trims_for_year_make_model()
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_trims_for_year_make_model(2024, 'Ford', 'f-150') LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Base, Crew Cab XLT, Custom, Eddie Bauer, FX2, FX4
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Query Performance: Makes Query
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "EXPLAIN ANALYZE SELECT DISTINCT make FROM vehicle_options WHERE year = 2024;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**:
|
|
||||||
- Execution Time: 1.527 ms
|
|
||||||
- Index used: idx_vehicle_year_make
|
|
||||||
- Status: EXCELLENT (well below 50ms target)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Quality: HTML Entity Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) as problematic_models FROM vehicle_options WHERE model LIKE '%&%';"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: problematic_models: 452
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Quality: HTML Entity Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT model FROM vehicle_options WHERE model LIKE '%&%' LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Results**:
|
|
||||||
- Ford Kuga Photos, engines & full specs
|
|
||||||
- Ford Mustang Dark Horse Photos, engines & full specs
|
|
||||||
- BMW X3 (G45) Photos, engines & full specs
|
|
||||||
- Chevrolet Colorado ZR2 Bison Photos, engines & full specs
|
|
||||||
- Audi RS3 Sedan Photos, engines & full specs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tesla (Electric Vehicle) Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM vehicle_options WHERE make = 'Tesla';"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: count: 4 (electric vehicles with NULL engine_id)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tesla Engine Data (NULL handling)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT DISTINCT vo.trim, e.id, e.name \
|
|
||||||
FROM vehicle_options vo \
|
|
||||||
LEFT JOIN engines e ON vo.engine_id = e.id \
|
|
||||||
WHERE vo.make = 'Tesla' AND vo.year = 2024 LIMIT 10;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Tesla Base trim has NULL engine_id (correctly handled for electric vehicles)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend Health Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-backend node -e "
|
|
||||||
const http = require('http');
|
|
||||||
const options = {
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: 3001,
|
|
||||||
path: '/api/vehicles/dropdown/years',
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer test-token'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => data += chunk);
|
|
||||||
res.on('end', () => {
|
|
||||||
console.log('Status:', res.statusCode);
|
|
||||||
console.log('Data:', data.substring(0, 200));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', (e) => console.error('Error:', e.message));
|
|
||||||
req.end();
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Status: 401 (expected - invalid token), confirming endpoint exists and requires authentication
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Container Health Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker ps --format "table {{.Names}}\t{{.Status}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**:
|
|
||||||
```
|
|
||||||
NAMES STATUS
|
|
||||||
mvp-frontend Up 3 days (healthy)
|
|
||||||
mvp-backend Up 3 days (healthy)
|
|
||||||
mvp-traefik Up 4 days (healthy)
|
|
||||||
mvp-postgres Up 4 days (healthy)
|
|
||||||
mvp-redis Up 4 days (healthy)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend HTTP Response
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs mvp-frontend 2>&1 | grep "GET / HTTP" | head -5
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: Multiple HTTP 200 responses confirming frontend is serving
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Corruption Percentage Calculation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT ROUND(100.0 * 452 / 1122644, 3) as percentage;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Result**: percentage: 0.040 (0.04% of records)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
All tests were executed directly against the running Docker containers:
|
|
||||||
- Database: mvp-postgres (Docker exec psql commands)
|
|
||||||
- Backend: mvp-backend (Docker exec node commands)
|
|
||||||
- Frontend: mvp-frontend (Container logs and HTTP checks)
|
|
||||||
|
|
||||||
All results confirm:
|
|
||||||
1. Database migration successful with 1.1M+ records
|
|
||||||
2. All API endpoints operational and secured
|
|
||||||
3. Query performance excellent (<2ms vs 50ms target)
|
|
||||||
4. 5 containers all healthy with continuous monitoring
|
|
||||||
5. Minor data quality issue (0.04%) from source data is non-blocking
|
|
||||||
|
|
||||||
Validation Date: 2025-11-11
|
|
||||||
Status: COMPLETE - READY FOR PRODUCTION
|
|
||||||
@@ -1,857 +0,0 @@
|
|||||||
# Backend Platform Repository Update - Agent 2
|
|
||||||
|
|
||||||
## Task: Update VehicleDataRepository to query new database schema
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agent 1 (Database Migration) must be complete
|
|
||||||
**Estimated Time**: 1-2 hours
|
|
||||||
**Assigned To**: Agent 2 (Platform Repository)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Replace normalized JOIN queries with queries against the new denormalized `vehicle_options` table. Change return types from `{id, name}[]` objects to `string[]` arrays.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify Database is Ready
|
|
||||||
```bash
|
|
||||||
# Confirm Agent 1 completed successfully
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM vehicle_options;"
|
|
||||||
# Should return: 1122644
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
```
|
|
||||||
backend/src/features/platform/data/vehicle-data.repository.ts
|
|
||||||
backend/src/features/platform/models/responses.ts (type definitions)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Implementation Analysis
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/data/vehicle-data.repository.ts`
|
|
||||||
|
|
||||||
**Current Methods** (ID-based, normalized queries):
|
|
||||||
```typescript
|
|
||||||
getYears(pool: Pool): Promise<number[]>
|
|
||||||
getMakes(pool: Pool, year: number): Promise<MakeItem[]>
|
|
||||||
getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]>
|
|
||||||
getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]>
|
|
||||||
getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]>
|
|
||||||
decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Current Type Definitions** (`models/responses.ts`):
|
|
||||||
```typescript
|
|
||||||
interface MakeItem { id: number; name: string; }
|
|
||||||
interface ModelItem { id: number; name: string; }
|
|
||||||
interface TrimItem { id: number; name: string; }
|
|
||||||
interface EngineItem { id: number; name: string; }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Update Type Definitions
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/models/responses.ts`
|
|
||||||
|
|
||||||
### Remove Old Interfaces
|
|
||||||
|
|
||||||
**Find and remove these interfaces**:
|
|
||||||
```typescript
|
|
||||||
export interface MakeItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrimItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EngineItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reason**: API will return string[] directly, no need for wrapper objects
|
|
||||||
|
|
||||||
**Note**: Keep `VINDecodeResult` interface - VIN decode may need separate handling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2: Update Repository Imports
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/data/vehicle-data.repository.ts`
|
|
||||||
|
|
||||||
**Current imports** (line 6):
|
|
||||||
```typescript
|
|
||||||
import { MakeItem, ModelItem, TrimItem, EngineItem } from '../models/responses';
|
|
||||||
```
|
|
||||||
|
|
||||||
**New imports**:
|
|
||||||
```typescript
|
|
||||||
import { VINDecodeResult } from '../models/responses';
|
|
||||||
// MakeItem, ModelItem, TrimItem, EngineItem removed - using string[] now
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Update getYears() Method
|
|
||||||
|
|
||||||
**Current Implementation** (lines 14-28):
|
|
||||||
```typescript
|
|
||||||
async getYears(pool: Pool): Promise<number[]> {
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT year
|
|
||||||
FROM vehicles.model_year
|
|
||||||
ORDER BY year DESC
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query);
|
|
||||||
return result.rows.map(row => row.year);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getYears', { error });
|
|
||||||
throw new Error('Failed to retrieve years from database');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Implementation**:
|
|
||||||
```typescript
|
|
||||||
async getYears(pool: Pool): Promise<number[]> {
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT year
|
|
||||||
FROM vehicle_options
|
|
||||||
ORDER BY year DESC
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query);
|
|
||||||
return result.rows.map(row => row.year);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getYears', { error });
|
|
||||||
throw new Error('Failed to retrieve years from database');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 16: `FROM vehicles.model_year` → `FROM vehicle_options`
|
|
||||||
- Return type unchanged (still `number[]`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Update getMakes() Method
|
|
||||||
|
|
||||||
**Current Implementation** (lines 33-52):
|
|
||||||
```typescript
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<MakeItem[]> {
|
|
||||||
const query = `
|
|
||||||
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
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year]);
|
|
||||||
return result.rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getMakes', { error, year });
|
|
||||||
throw new Error(`Failed to retrieve makes for year ${year}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Implementation**:
|
|
||||||
```typescript
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<string[]> {
|
|
||||||
// Use database function for optimal performance
|
|
||||||
const query = `
|
|
||||||
SELECT make FROM get_makes_for_year($1)
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year]);
|
|
||||||
return result.rows.map(row => row.make);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getMakes', { error, year });
|
|
||||||
throw new Error(`Failed to retrieve makes for year ${year}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Return type: `Promise<MakeItem[]>` → `Promise<string[]>`
|
|
||||||
- Query: Use `get_makes_for_year()` database function
|
|
||||||
- Result mapping: Return string directly (not {id, name} object)
|
|
||||||
|
|
||||||
**Alternative** (if not using database function):
|
|
||||||
```typescript
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT make
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE year = $1
|
|
||||||
ORDER BY make
|
|
||||||
`;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5: Update getModels() Method
|
|
||||||
|
|
||||||
**Current Implementation** (lines 57-76):
|
|
||||||
```typescript
|
|
||||||
async getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]> {
|
|
||||||
const query = `
|
|
||||||
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
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, makeId]);
|
|
||||||
return result.rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getModels', { error, year, makeId });
|
|
||||||
throw new Error(`Failed to retrieve models for year ${year}, make ${makeId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Implementation**:
|
|
||||||
```typescript
|
|
||||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> {
|
|
||||||
// Use database function for optimal performance
|
|
||||||
const query = `
|
|
||||||
SELECT model FROM get_models_for_year_make($1, $2)
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, make]);
|
|
||||||
return result.rows.map(row => row.model);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getModels', { error, year, make });
|
|
||||||
throw new Error(`Failed to retrieve models for year ${year}, make ${make}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Parameter: `makeId: number` → `make: string`
|
|
||||||
- Return type: `Promise<ModelItem[]>` → `Promise<string[]>`
|
|
||||||
- Query: Use `get_models_for_year_make()` database function
|
|
||||||
- Result mapping: Return string directly
|
|
||||||
|
|
||||||
**Alternative** (if not using database function):
|
|
||||||
```typescript
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT model
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE year = $1 AND make = $2
|
|
||||||
ORDER BY model
|
|
||||||
`;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6: Update getTrims() Method
|
|
||||||
|
|
||||||
**Current Implementation** (lines 81-100):
|
|
||||||
```typescript
|
|
||||||
async getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]> {
|
|
||||||
const query = `
|
|
||||||
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
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, modelId]);
|
|
||||||
return result.rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getTrims', { error, year, modelId });
|
|
||||||
throw new Error(`Failed to retrieve trims for year ${year}, model ${modelId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Implementation**:
|
|
||||||
```typescript
|
|
||||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
// Use database function for optimal performance
|
|
||||||
const query = `
|
|
||||||
SELECT trim_name FROM get_trims_for_year_make_model($1, $2, $3)
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, make, model]);
|
|
||||||
return result.rows.map(row => row.trim_name);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getTrims', { error, year, make, model });
|
|
||||||
throw new Error(`Failed to retrieve trims for year ${year}, make ${make}, model ${model}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Parameters: Added `make: string`, changed `modelId: number` → `model: string`
|
|
||||||
- Return type: `Promise<TrimItem[]>` → `Promise<string[]>`
|
|
||||||
- Query: Use `get_trims_for_year_make_model()` database function
|
|
||||||
- Result mapping: Return string directly
|
|
||||||
|
|
||||||
**Alternative** (if not using database function):
|
|
||||||
```typescript
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT trim
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE year = $1 AND make = $2 AND model = $3
|
|
||||||
ORDER BY trim
|
|
||||||
`;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7: Update getEngines() Method
|
|
||||||
|
|
||||||
**Current Implementation** (lines 105-128):
|
|
||||||
```typescript
|
|
||||||
async getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]> {
|
|
||||||
const query = `
|
|
||||||
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
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, modelId, trimId]);
|
|
||||||
return result.rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getEngines', { error, year, modelId, trimId });
|
|
||||||
throw new Error(`Failed to retrieve engines for year ${year}, model ${modelId}, trim ${trimId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Implementation**:
|
|
||||||
```typescript
|
|
||||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> {
|
|
||||||
// Query vehicle_options and join with engines table
|
|
||||||
// Handle NULL engine_id for electric vehicles
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT
|
|
||||||
CASE
|
|
||||||
WHEN vo.engine_id IS NULL THEN 'N/A (Electric)'
|
|
||||||
ELSE e.name
|
|
||||||
END as engine_name
|
|
||||||
FROM vehicle_options vo
|
|
||||||
LEFT JOIN engines e ON e.id = vo.engine_id
|
|
||||||
WHERE vo.year = $1
|
|
||||||
AND vo.make = $2
|
|
||||||
AND vo.model = $3
|
|
||||||
AND vo.trim = $4
|
|
||||||
ORDER BY engine_name
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, make, model, trim]);
|
|
||||||
return result.rows.map(row => row.engine_name);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getEngines', { error, year, make, model, trim });
|
|
||||||
throw new Error(`Failed to retrieve engines for year ${year}, make ${make}, model ${model}, trim ${trim}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Parameters: Changed from IDs to strings (`make`, `model`, `trim`)
|
|
||||||
- Return type: `Promise<EngineItem[]>` → `Promise<string[]>`
|
|
||||||
- Query: Direct query on `vehicle_options` with LEFT JOIN to `engines`
|
|
||||||
- **NULL Handling**: Returns 'N/A (Electric)' for NULL engine_id
|
|
||||||
- Result mapping: Return string directly
|
|
||||||
|
|
||||||
**Alternative using database function**:
|
|
||||||
```typescript
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN engine_name IS NULL THEN 'N/A (Electric)'
|
|
||||||
ELSE engine_name
|
|
||||||
END as engine_name
|
|
||||||
FROM get_options_for_vehicle($1, $2, $3, $4)
|
|
||||||
`;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 8: Add getTransmissions() Method (NEW)
|
|
||||||
|
|
||||||
**Add this new method** after `getEngines()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* Get transmissions for a specific year, make, and model
|
|
||||||
* Note: Transmissions are tied to model, not trim
|
|
||||||
*/
|
|
||||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
// Query vehicle_options and join with transmissions table
|
|
||||||
// Handle NULL transmission_id for some vehicles
|
|
||||||
const query = `
|
|
||||||
SELECT DISTINCT
|
|
||||||
CASE
|
|
||||||
WHEN vo.transmission_id IS NULL THEN 'N/A'
|
|
||||||
ELSE t.type
|
|
||||||
END as transmission_type
|
|
||||||
FROM vehicle_options vo
|
|
||||||
LEFT JOIN transmissions t ON t.id = vo.transmission_id
|
|
||||||
WHERE vo.year = $1
|
|
||||||
AND vo.make = $2
|
|
||||||
AND vo.model = $3
|
|
||||||
ORDER BY transmission_type
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pool.query(query, [year, make, model]);
|
|
||||||
return result.rows.map(row => row.transmission_type);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Repository error: getTransmissions', { error, year, make, model });
|
|
||||||
throw new Error(`Failed to retrieve transmissions for year ${year}, make ${make}, model ${model}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why This is New**:
|
|
||||||
- Old system returned hardcoded ["Automatic", "Manual"]
|
|
||||||
- New database has 828 real transmission types
|
|
||||||
- Returns actual data: "8-Speed Automatic", "6-Speed Manual", "CVT", etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 9: Handle VIN Decode (IMPORTANT)
|
|
||||||
|
|
||||||
**Current Implementation** (lines 133-164):
|
|
||||||
Uses `vehicles.f_decode_vin()` function which may not exist after migration.
|
|
||||||
|
|
||||||
**Decision Required**:
|
|
||||||
The VIN decode function depends on the old vehicles.* schema. You have two options:
|
|
||||||
|
|
||||||
### Option A: Leave VIN Decode Unchanged (Recommended for now)
|
|
||||||
```typescript
|
|
||||||
// Keep decodeVIN() method exactly as-is
|
|
||||||
// Agent 8 will investigate and fix if broken
|
|
||||||
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null> {
|
|
||||||
// ... existing implementation unchanged
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Add a comment**:
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* Decode VIN using PostgreSQL function
|
|
||||||
* NOTE: This function may need updates after vehicles.* schema migration
|
|
||||||
* See Agent 8 investigation results
|
|
||||||
*/
|
|
||||||
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null> {
|
|
||||||
// ... existing code
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option B: Stub Out VIN Decode (If Definitely Broken)
|
|
||||||
```typescript
|
|
||||||
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null> {
|
|
||||||
// TODO: Agent 8 - Implement VIN decode with new schema
|
|
||||||
logger.warn('VIN decode not yet implemented for new schema', { vin });
|
|
||||||
throw new Error('VIN decode temporarily unavailable during migration');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Recommendation**: Use Option A initially. Test after changes, then decide.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 10: Update Documentation Comments
|
|
||||||
|
|
||||||
Update the file header comment:
|
|
||||||
|
|
||||||
**Old** (lines 1-4):
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data repository for hierarchical queries
|
|
||||||
* @ai-context PostgreSQL queries against vehicles schema
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data repository for hierarchical dropdown queries
|
|
||||||
* @ai-context Queries denormalized vehicle_options table with string-based cascade
|
|
||||||
* @ai-migration Updated to use new ETL-generated database (1.1M+ vehicle configs)
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Complete Updated File Structure
|
|
||||||
|
|
||||||
After all changes, the file should look like:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data repository for hierarchical dropdown queries
|
|
||||||
* @ai-context Queries denormalized vehicle_options table with string-based cascade
|
|
||||||
* @ai-migration Updated to use new ETL-generated database (1.1M+ vehicle configs)
|
|
||||||
*/
|
|
||||||
import { Pool } from 'pg';
|
|
||||||
import { VINDecodeResult } from '../models/responses';
|
|
||||||
import { logger } from '../../../core/logging/logger';
|
|
||||||
|
|
||||||
export class VehicleDataRepository {
|
|
||||||
// Returns: number[]
|
|
||||||
async getYears(pool: Pool): Promise<number[]> { ... }
|
|
||||||
|
|
||||||
// Returns: string[] (not MakeItem[])
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
// Parameters: year, make (not makeId)
|
|
||||||
// Returns: string[] (not ModelItem[])
|
|
||||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
// Parameters: year, make, model (not year, modelId)
|
|
||||||
// Returns: string[] (not TrimItem[])
|
|
||||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
// Parameters: year, make, model, trim (all strings except year)
|
|
||||||
// Returns: string[] with 'N/A (Electric)' for NULL
|
|
||||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
// NEW METHOD - Real transmission data (not hardcoded)
|
|
||||||
// Parameters: year, make, model
|
|
||||||
// Returns: string[] like "8-Speed Automatic", "CVT"
|
|
||||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
// VIN decode - may need updates (see Agent 8)
|
|
||||||
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null> { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
After making changes, test each method:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start a Node REPL in the backend container
|
|
||||||
docker exec -it mvp-backend node
|
|
||||||
|
|
||||||
# In Node REPL:
|
|
||||||
const { Pool } = require('pg');
|
|
||||||
const { VehicleDataRepository } = require('./src/features/platform/data/vehicle-data.repository');
|
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
host: 'postgres',
|
|
||||||
port: 5432,
|
|
||||||
database: 'motovaultpro',
|
|
||||||
user: 'postgres',
|
|
||||||
password: process.env.POSTGRES_PASSWORD
|
|
||||||
});
|
|
||||||
|
|
||||||
const repo = new VehicleDataRepository();
|
|
||||||
|
|
||||||
// Test getYears
|
|
||||||
repo.getYears(pool).then(console.log);
|
|
||||||
// Expected: [2026, 2025, 2024, ..., 1980]
|
|
||||||
|
|
||||||
// Test getMakes
|
|
||||||
repo.getMakes(pool, 2024).then(console.log);
|
|
||||||
// Expected: ["Acura", "Audi", "BMW", "Ford", ...]
|
|
||||||
|
|
||||||
// Test getModels
|
|
||||||
repo.getModels(pool, 2024, 'Ford').then(console.log);
|
|
||||||
// Expected: ["Bronco", "Edge", "Escape", "Explorer", "F-150", ...]
|
|
||||||
|
|
||||||
// Test getTrims
|
|
||||||
repo.getTrims(pool, 2024, 'Ford', 'F-150').then(console.log);
|
|
||||||
// Expected: ["King Ranch", "Lariat", "Limited", "Platinum", "XL", "XLT", ...]
|
|
||||||
|
|
||||||
// Test getEngines (with trim)
|
|
||||||
repo.getEngines(pool, 2024, 'Ford', 'F-150', 'XLT').then(console.log);
|
|
||||||
// Expected: ["V6 2.7L Turbo", "V6 3.5L Turbo", "V8 5.0L", ...]
|
|
||||||
|
|
||||||
// Test getTransmissions
|
|
||||||
repo.getTransmissions(pool, 2024, 'Ford', 'F-150').then(console.log);
|
|
||||||
// Expected: ["10-Speed Automatic", "6-Speed Automatic", ...]
|
|
||||||
|
|
||||||
// Test with electric vehicle (should show N/A)
|
|
||||||
repo.getEngines(pool, 2024, 'Tesla', 'Model 3', 'Standard Range').then(console.log);
|
|
||||||
// Expected: ["N/A (Electric)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
Create or update unit tests:
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/tests/unit/vehicle-data.repository.test.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { VehicleDataRepository } from '../../data/vehicle-data.repository';
|
|
||||||
import { Pool } from 'pg';
|
|
||||||
|
|
||||||
describe('VehicleDataRepository', () => {
|
|
||||||
let repo: VehicleDataRepository;
|
|
||||||
let mockPool: jest.Mocked<Pool>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
repo = new VehicleDataRepository();
|
|
||||||
mockPool = {
|
|
||||||
query: jest.fn()
|
|
||||||
} as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMakes', () => {
|
|
||||||
it('should return string array of makes', async () => {
|
|
||||||
mockPool.query.mockResolvedValue({
|
|
||||||
rows: [{ make: 'Ford' }, { make: 'Honda' }]
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await repo.getMakes(mockPool, 2024);
|
|
||||||
|
|
||||||
expect(result).toEqual(['Ford', 'Honda']);
|
|
||||||
expect(mockPool.query).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('get_makes_for_year'),
|
|
||||||
[2024]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getModels', () => {
|
|
||||||
it('should accept make string parameter', async () => {
|
|
||||||
mockPool.query.mockResolvedValue({
|
|
||||||
rows: [{ model: 'F-150' }, { model: 'Mustang' }]
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await repo.getModels(mockPool, 2024, 'Ford');
|
|
||||||
|
|
||||||
expect(result).toEqual(['F-150', 'Mustang']);
|
|
||||||
expect(mockPool.query).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[2024, 'Ford']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEngines', () => {
|
|
||||||
it('should handle NULL engine_id as "N/A (Electric)"', async () => {
|
|
||||||
mockPool.query.mockResolvedValue({
|
|
||||||
rows: [{ engine_name: 'N/A (Electric)' }]
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await repo.getEngines(mockPool, 2024, 'Tesla', 'Model 3', 'Standard Range');
|
|
||||||
|
|
||||||
expect(result).toContain('N/A (Electric)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTransmissions', () => {
|
|
||||||
it('should return real transmission data', async () => {
|
|
||||||
mockPool.query.mockResolvedValue({
|
|
||||||
rows: [
|
|
||||||
{ transmission_type: '10-Speed Automatic' },
|
|
||||||
{ transmission_type: '6-Speed Manual' }
|
|
||||||
]
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await repo.getTransmissions(mockPool, 2024, 'Ford', 'Mustang');
|
|
||||||
|
|
||||||
expect(result).toEqual(['10-Speed Automatic', '6-Speed Manual']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
npm test -- vehicle-data.repository.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion:
|
|
||||||
|
|
||||||
- [ ] All type imports updated (removed MakeItem, ModelItem, etc.)
|
|
||||||
- [ ] getYears() queries vehicle_options table
|
|
||||||
- [ ] getMakes() returns string[] and accepts year parameter
|
|
||||||
- [ ] getModels() returns string[] and accepts year + make (string)
|
|
||||||
- [ ] getTrims() returns string[] and accepts year + make + model (strings)
|
|
||||||
- [ ] getEngines() returns string[] with NULL handling ('N/A (Electric)')
|
|
||||||
- [ ] getTransmissions() method added (new)
|
|
||||||
- [ ] VIN decode method addressed (left unchanged with note OR stubbed)
|
|
||||||
- [ ] File documentation comments updated
|
|
||||||
- [ ] Manual testing completed successfully
|
|
||||||
- [ ] Unit tests pass (or updated to match new signatures)
|
|
||||||
- [ ] No TypeScript compilation errors
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: "function get_makes_for_year does not exist"
|
|
||||||
**Cause**: Database migration not complete or functions not created
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Verify functions exist
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "\df get_makes_for_year"
|
|
||||||
|
|
||||||
# If not found, Agent 1 needs to re-run migration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: "column vo.make does not exist"
|
|
||||||
**Cause**: Old vehicles.* tables still present, vehicle_options not created
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Check which tables exist
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "\dt"
|
|
||||||
|
|
||||||
# Should see: engines, transmissions, vehicle_options
|
|
||||||
# If not, Agent 1 needs to complete database migration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: TypeScript errors about parameter types
|
|
||||||
**Cause**: Service layer (Agent 3) still using old signatures
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- This is expected - Agent 3 will fix service layer
|
|
||||||
- Ensure your repository signatures are correct (strings not numbers)
|
|
||||||
- Agent 3 will update service to match your new signatures
|
|
||||||
|
|
||||||
### Issue: Empty results returned
|
|
||||||
**Cause**: Case sensitivity or formatting mismatch
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Check actual data format in database
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT DISTINCT make FROM vehicle_options LIMIT 10;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Verify makes are in Title Case: "Ford" not "FORD" or "ford"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handoff to Agent 3
|
|
||||||
|
|
||||||
Once complete, provide this information:
|
|
||||||
|
|
||||||
### Updated Repository Contract
|
|
||||||
|
|
||||||
**Methods:**
|
|
||||||
```typescript
|
|
||||||
getYears(pool: Pool): Promise<number[]>
|
|
||||||
getMakes(pool: Pool, year: number): Promise<string[]>
|
|
||||||
getModels(pool: Pool, year: number, make: string): Promise<string[]>
|
|
||||||
getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]>
|
|
||||||
getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]>
|
|
||||||
getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]>
|
|
||||||
decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Changes for Agent 3**:
|
|
||||||
- All dropdown methods return `string[]` (not objects)
|
|
||||||
- Parameters use strings (make, model, trim) not IDs (makeId, modelId, trimId)
|
|
||||||
- `getTransmissions()` is a new method (was hardcoded before)
|
|
||||||
- NULL engines display as 'N/A (Electric)'
|
|
||||||
|
|
||||||
### Verification Command
|
|
||||||
```bash
|
|
||||||
# Agent 3 can verify repository is ready:
|
|
||||||
cd backend && npm run build
|
|
||||||
# Should compile with no errors in vehicle-data.repository.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 2 (Platform Repository): COMPLETE
|
|
||||||
|
|
||||||
Files Modified:
|
|
||||||
- backend/src/features/platform/data/vehicle-data.repository.ts
|
|
||||||
- backend/src/features/platform/models/responses.ts
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Updated all dropdown methods to return string[] (not objects)
|
|
||||||
- Changed parameters from IDs to strings (make, model, trim)
|
|
||||||
- Query vehicle_options table using database functions
|
|
||||||
- Added NULL handling for electric vehicles ('N/A (Electric)')
|
|
||||||
- Added new getTransmissions() method with real data
|
|
||||||
- VIN decode left unchanged (pending Agent 8 investigation)
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ TypeScript compiles successfully
|
|
||||||
✓ Manual tests return correct data
|
|
||||||
✓ Unit tests pass
|
|
||||||
✓ Electric vehicles show 'N/A (Electric)' for engines
|
|
||||||
✓ Transmissions return real types (not hardcoded)
|
|
||||||
|
|
||||||
Agent 3 (Platform Service) can now update service layer to use new repository signatures.
|
|
||||||
|
|
||||||
Breaking Changes for Agent 3:
|
|
||||||
- All return types changed to string[]
|
|
||||||
- Parameter names changed: makeId→make, modelId→model, trimId→trim
|
|
||||||
- New method: getTransmissions()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,739 +0,0 @@
|
|||||||
# Backend Platform Service Update - Agent 3
|
|
||||||
|
|
||||||
## Task: Update VehicleDataService to use string-based signatures
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agent 2 (Platform Repository) must be complete
|
|
||||||
**Estimated Time**: 1 hour
|
|
||||||
**Assigned To**: Agent 3 (Platform Service)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Update the service layer to match the new repository signatures. Change from ID-based parameters to string-based parameters, and update return types from objects to string arrays.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify Agent 2 Completed
|
|
||||||
```bash
|
|
||||||
# Verify repository compiles
|
|
||||||
cd backend && npm run build
|
|
||||||
|
|
||||||
# Should see no errors in vehicle-data.repository.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
```
|
|
||||||
backend/src/features/platform/domain/vehicle-data.service.ts
|
|
||||||
backend/src/features/platform/domain/platform-cache.service.ts (cache keys)
|
|
||||||
backend/src/features/platform/models/responses.ts (if not already updated)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Implementation Analysis
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/domain/vehicle-data.service.ts`
|
|
||||||
|
|
||||||
**Current Methods**:
|
|
||||||
```typescript
|
|
||||||
getYears(pool: Pool): Promise<number[]>
|
|
||||||
getMakes(pool: Pool, year: number): Promise<MakeItem[]>
|
|
||||||
getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]>
|
|
||||||
getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]>
|
|
||||||
getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern**: Each method has:
|
|
||||||
1. Cache lookup
|
|
||||||
2. Repository call if cache miss
|
|
||||||
3. Cache storage
|
|
||||||
4. Logging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Update Imports
|
|
||||||
|
|
||||||
**Current** (line 8):
|
|
||||||
```typescript
|
|
||||||
import { MakeItem, ModelItem, TrimItem, EngineItem } from '../models/responses';
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
import { VINDecodeResult } from '../models/responses';
|
|
||||||
// MakeItem, ModelItem, TrimItem, EngineItem removed - using string[] now
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2: Update getMakes() Method
|
|
||||||
|
|
||||||
**Current** (lines 44-60):
|
|
||||||
```typescript
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<MakeItem[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getMakes(year);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Makes retrieved from cache', { year });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makes = await this.repository.getMakes(pool, year);
|
|
||||||
await this.cache.setMakes(year, makes);
|
|
||||||
logger.debug('Makes retrieved from database and cached', { year, count: makes.length });
|
|
||||||
return makes;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getMakes', { error, year });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getMakes(year);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Makes retrieved from cache', { year });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makes = await this.repository.getMakes(pool, year);
|
|
||||||
await this.cache.setMakes(year, makes);
|
|
||||||
logger.debug('Makes retrieved from database and cached', { year, count: makes.length });
|
|
||||||
return makes;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getMakes', { error, year });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 44: Return type `Promise<MakeItem[]>` → `Promise<string[]>`
|
|
||||||
- Logic unchanged (cache still works the same way)
|
|
||||||
|
|
||||||
**Note**: Cache service will need updates (Step 7)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Update getModels() Method
|
|
||||||
|
|
||||||
**Current** (lines 65-81):
|
|
||||||
```typescript
|
|
||||||
async getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getModels(year, makeId);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Models retrieved from cache', { year, makeId });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = await this.repository.getModels(pool, year, makeId);
|
|
||||||
await this.cache.setModels(year, makeId, models);
|
|
||||||
logger.debug('Models retrieved from database and cached', { year, makeId, count: models.length });
|
|
||||||
return models;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getModels', { error, year, makeId });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getModels(year, make);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Models retrieved from cache', { year, make });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = await this.repository.getModels(pool, year, make);
|
|
||||||
await this.cache.setModels(year, make, models);
|
|
||||||
logger.debug('Models retrieved from database and cached', { year, make, count: models.length });
|
|
||||||
return models;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getModels', { error, year, make });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 65: Parameter `makeId: number` → `make: string`
|
|
||||||
- Line 65: Return type `Promise<ModelItem[]>` → `Promise<string[]>`
|
|
||||||
- Line 67: Cache call uses `make` instead of `makeId`
|
|
||||||
- Line 69: Logger uses `make` instead of `makeId`
|
|
||||||
- Line 73: Repository call uses `make` instead of `makeId`
|
|
||||||
- Line 74: Cache set uses `make` instead of `makeId`
|
|
||||||
- Line 75: Logger uses `make` instead of `makeId`
|
|
||||||
- Line 78: Logger uses `make` instead of `makeId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Update getTrims() Method
|
|
||||||
|
|
||||||
**Current** (lines 86-102):
|
|
||||||
```typescript
|
|
||||||
async getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getTrims(year, modelId);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Trims retrieved from cache', { year, modelId });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trims = await this.repository.getTrims(pool, year, modelId);
|
|
||||||
await this.cache.setTrims(year, modelId, trims);
|
|
||||||
logger.debug('Trims retrieved from database and cached', { year, modelId, count: trims.length });
|
|
||||||
return trims;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getTrims', { error, year, modelId });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getTrims(year, make, model);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Trims retrieved from cache', { year, make, model });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trims = await this.repository.getTrims(pool, year, make, model);
|
|
||||||
await this.cache.setTrims(year, make, model, trims);
|
|
||||||
logger.debug('Trims retrieved from database and cached', { year, make, model, count: trims.length });
|
|
||||||
return trims;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getTrims', { error, year, make, model });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 86: Parameters changed from `modelId: number` to `make: string, model: string`
|
|
||||||
- Line 86: Return type `Promise<TrimItem[]>` → `Promise<string[]>`
|
|
||||||
- All cache and logger calls updated to use `make, model` instead of `modelId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5: Update getEngines() Method
|
|
||||||
|
|
||||||
**Current** (lines 107-123):
|
|
||||||
```typescript
|
|
||||||
async getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getEngines(year, modelId, trimId);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Engines retrieved from cache', { year, modelId, trimId });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const engines = await this.repository.getEngines(pool, year, modelId, trimId);
|
|
||||||
await this.cache.setEngines(year, modelId, trimId, engines);
|
|
||||||
logger.debug('Engines retrieved from database and cached', { year, modelId, trimId, count: engines.length });
|
|
||||||
return engines;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getEngines', { error, year, modelId, trimId });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getEngines(year, make, model, trim);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Engines retrieved from cache', { year, make, model, trim });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const engines = await this.repository.getEngines(pool, year, make, model, trim);
|
|
||||||
await this.cache.setEngines(year, make, model, trim, engines);
|
|
||||||
logger.debug('Engines retrieved from database and cached', { year, make, model, trim, count: engines.length });
|
|
||||||
return engines;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getEngines', { error, year, make, model, trim });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 107: Parameters changed from `modelId: number, trimId: number` to `make: string, model: string, trim: string`
|
|
||||||
- Line 107: Return type `Promise<EngineItem[]>` → `Promise<string[]>`
|
|
||||||
- All cache and logger calls updated to use `make, model, trim` instead of `modelId, trimId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6: Add getTransmissions() Method (NEW)
|
|
||||||
|
|
||||||
Add this new method after `getEngines()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* Get transmissions for a year, make, and model with caching
|
|
||||||
*/
|
|
||||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const cached = await this.cache.getTransmissions(year, make, model);
|
|
||||||
if (cached) {
|
|
||||||
logger.debug('Transmissions retrieved from cache', { year, make, model });
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transmissions = await this.repository.getTransmissions(pool, year, make, model);
|
|
||||||
await this.cache.setTransmissions(year, make, model, transmissions);
|
|
||||||
logger.debug('Transmissions retrieved from database and cached', { year, make, model, count: transmissions.length });
|
|
||||||
return transmissions;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Service error: getTransmissions', { error, year, make, model });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why This is New**:
|
|
||||||
- Repository added `getTransmissions()` method (real data, not hardcoded)
|
|
||||||
- Service needs to expose this with caching
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7: Update Cache Service
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/domain/platform-cache.service.ts`
|
|
||||||
|
|
||||||
The cache service needs updates to handle string-based cache keys instead of ID-based keys.
|
|
||||||
|
|
||||||
### Cache Key Changes
|
|
||||||
|
|
||||||
**Current cache keys**:
|
|
||||||
```typescript
|
|
||||||
`years`
|
|
||||||
`makes:${year}`
|
|
||||||
`models:${year}:${makeId}`
|
|
||||||
`trims:${year}:${modelId}`
|
|
||||||
`engines:${year}:${modelId}:${trimId}`
|
|
||||||
```
|
|
||||||
|
|
||||||
**New cache keys**:
|
|
||||||
```typescript
|
|
||||||
`years`
|
|
||||||
`makes:${year}`
|
|
||||||
`models:${year}:${make}`
|
|
||||||
`trims:${year}:${make}:${model}`
|
|
||||||
`engines:${year}:${make}:${model}:${trim}`
|
|
||||||
`transmissions:${year}:${make}:${model}` // NEW
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Cache Method Signatures
|
|
||||||
|
|
||||||
**Find and update these methods**:
|
|
||||||
|
|
||||||
#### getMakes / setMakes
|
|
||||||
```typescript
|
|
||||||
// Signature unchanged (still takes year: number)
|
|
||||||
async getMakes(year: number): Promise<string[] | null>
|
|
||||||
async setMakes(year: number, makes: string[]): Promise<void>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getModels / setModels
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
async getModels(year: number, makeId: number): Promise<ModelItem[] | null>
|
|
||||||
async setModels(year: number, makeId: number, models: ModelItem[]): Promise<void>
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
async getModels(year: number, make: string): Promise<string[] | null>
|
|
||||||
async setModels(year: number, make: string, models: string[]): Promise<void>
|
|
||||||
|
|
||||||
// Implementation change:
|
|
||||||
const key = `models:${year}:${make}`; // was makeId
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getTrims / setTrims
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
async getTrims(year: number, modelId: number): Promise<TrimItem[] | null>
|
|
||||||
async setTrims(year: number, modelId: number, trims: TrimItem[]): Promise<void>
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
async getTrims(year: number, make: string, model: string): Promise<string[] | null>
|
|
||||||
async setTrims(year: number, make: string, model: string, trims: string[]): Promise<void>
|
|
||||||
|
|
||||||
// Implementation change:
|
|
||||||
const key = `trims:${year}:${make}:${model}`; // was modelId only
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getEngines / setEngines
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
async getEngines(year: number, modelId: number, trimId: number): Promise<EngineItem[] | null>
|
|
||||||
async setEngines(year: number, modelId: number, trimId: number, engines: EngineItem[]): Promise<void>
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
async getEngines(year: number, make: string, model: string, trim: string): Promise<string[] | null>
|
|
||||||
async setEngines(year: number, make: string, model: string, trim: string, engines: string[]): Promise<void>
|
|
||||||
|
|
||||||
// Implementation change:
|
|
||||||
const key = `engines:${year}:${make}:${model}:${trim}`; // was modelId:trimId
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getTransmissions / setTransmissions (NEW)
|
|
||||||
```typescript
|
|
||||||
async getTransmissions(year: number, make: string, model: string): Promise<string[] | null> {
|
|
||||||
const key = `transmissions:${year}:${make}:${model}`;
|
|
||||||
const cached = await this.redisClient.get(key);
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
return JSON.parse(cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setTransmissions(year: number, make: string, model: string, transmissions: string[]): Promise<void> {
|
|
||||||
const key = `transmissions:${year}:${make}:${model}`;
|
|
||||||
await this.redisClient.setex(key, this.TTL, JSON.stringify(transmissions));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 8: Update Documentation Comments
|
|
||||||
|
|
||||||
Update the file header:
|
|
||||||
|
|
||||||
**Old** (lines 1-4):
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data service with caching
|
|
||||||
* @ai-context Business logic for hierarchical vehicle data queries
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data service with caching for dropdown queries
|
|
||||||
* @ai-context String-based cascade queries with Redis caching
|
|
||||||
* @ai-migration Updated to use string parameters (not IDs)
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Complete Updated Service Structure
|
|
||||||
|
|
||||||
After all changes:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary Vehicle data service with caching for dropdown queries
|
|
||||||
* @ai-context String-based cascade queries with Redis caching
|
|
||||||
* @ai-migration Updated to use string parameters (not IDs)
|
|
||||||
*/
|
|
||||||
import { Pool } from 'pg';
|
|
||||||
import { VehicleDataRepository } from '../data/vehicle-data.repository';
|
|
||||||
import { PlatformCacheService } from './platform-cache.service';
|
|
||||||
import { VINDecodeResult } from '../models/responses';
|
|
||||||
import { logger } from '../../../core/logging/logger';
|
|
||||||
|
|
||||||
export class VehicleDataService {
|
|
||||||
private repository: VehicleDataRepository;
|
|
||||||
private cache: PlatformCacheService;
|
|
||||||
|
|
||||||
constructor(cache: PlatformCacheService, repository?: VehicleDataRepository) {
|
|
||||||
this.cache = cache;
|
|
||||||
this.repository = repository || new VehicleDataRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getYears(pool: Pool): Promise<number[]> { ... } // Unchanged
|
|
||||||
|
|
||||||
async getMakes(pool: Pool, year: number): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> { ... }
|
|
||||||
|
|
||||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> { ... } // NEW
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
Update existing tests:
|
|
||||||
|
|
||||||
**File**: `backend/src/features/platform/tests/unit/vehicle-data.service.test.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { VehicleDataService } from '../../domain/vehicle-data.service';
|
|
||||||
import { VehicleDataRepository } from '../../data/vehicle-data.repository';
|
|
||||||
import { PlatformCacheService } from '../../domain/platform-cache.service';
|
|
||||||
|
|
||||||
describe('VehicleDataService', () => {
|
|
||||||
let service: VehicleDataService;
|
|
||||||
let mockRepository: jest.Mocked<VehicleDataRepository>;
|
|
||||||
let mockCache: jest.Mocked<PlatformCacheService>;
|
|
||||||
let mockPool: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockRepository = {
|
|
||||||
getMakes: jest.fn(),
|
|
||||||
getModels: jest.fn(),
|
|
||||||
getTrims: jest.fn(),
|
|
||||||
getEngines: jest.fn(),
|
|
||||||
getTransmissions: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
mockCache = {
|
|
||||||
getMakes: jest.fn(),
|
|
||||||
setMakes: jest.fn(),
|
|
||||||
getModels: jest.fn(),
|
|
||||||
setModels: jest.fn(),
|
|
||||||
// ... etc
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
service = new VehicleDataService(mockCache, mockRepository);
|
|
||||||
mockPool = {} as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMakes', () => {
|
|
||||||
it('should return string array from cache', async () => {
|
|
||||||
mockCache.getMakes.mockResolvedValue(['Ford', 'Honda']);
|
|
||||||
|
|
||||||
const result = await service.getMakes(mockPool, 2024);
|
|
||||||
|
|
||||||
expect(result).toEqual(['Ford', 'Honda']);
|
|
||||||
expect(mockRepository.getMakes).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch from repository on cache miss', async () => {
|
|
||||||
mockCache.getMakes.mockResolvedValue(null);
|
|
||||||
mockRepository.getMakes.mockResolvedValue(['Ford', 'Honda']);
|
|
||||||
|
|
||||||
const result = await service.getMakes(mockPool, 2024);
|
|
||||||
|
|
||||||
expect(result).toEqual(['Ford', 'Honda']);
|
|
||||||
expect(mockRepository.getMakes).toHaveBeenCalledWith(mockPool, 2024);
|
|
||||||
expect(mockCache.setMakes).toHaveBeenCalledWith(2024, ['Ford', 'Honda']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getModels', () => {
|
|
||||||
it('should accept make string parameter', async () => {
|
|
||||||
mockCache.getModels.mockResolvedValue(null);
|
|
||||||
mockRepository.getModels.mockResolvedValue(['F-150', 'Mustang']);
|
|
||||||
|
|
||||||
const result = await service.getModels(mockPool, 2024, 'Ford');
|
|
||||||
|
|
||||||
expect(result).toEqual(['F-150', 'Mustang']);
|
|
||||||
expect(mockRepository.getModels).toHaveBeenCalledWith(mockPool, 2024, 'Ford');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTransmissions', () => {
|
|
||||||
it('should return transmission data', async () => {
|
|
||||||
mockCache.getTransmissions.mockResolvedValue(null);
|
|
||||||
mockRepository.getTransmissions.mockResolvedValue(['10-Speed Automatic']);
|
|
||||||
|
|
||||||
const result = await service.getTransmissions(mockPool, 2024, 'Ford', 'F-150');
|
|
||||||
|
|
||||||
expect(result).toEqual(['10-Speed Automatic']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
npm test -- vehicle-data.service.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Test
|
|
||||||
|
|
||||||
Test the full flow with real cache:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start backend container
|
|
||||||
make start
|
|
||||||
|
|
||||||
# Access backend container
|
|
||||||
docker exec -it mvp-backend node
|
|
||||||
|
|
||||||
# In Node REPL:
|
|
||||||
const { Pool } = require('pg');
|
|
||||||
const { VehicleDataService } = require('./src/features/platform/domain/vehicle-data.service');
|
|
||||||
const { VehicleDataRepository } = require('./src/features/platform/data/vehicle-data.repository');
|
|
||||||
const { PlatformCacheService } = require('./src/features/platform/domain/platform-cache.service');
|
|
||||||
const Redis = require('ioredis');
|
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
host: 'postgres',
|
|
||||||
database: 'motovaultpro',
|
|
||||||
user: 'postgres',
|
|
||||||
password: process.env.POSTGRES_PASSWORD
|
|
||||||
});
|
|
||||||
|
|
||||||
const redis = new Redis({ host: 'redis' });
|
|
||||||
const cache = new PlatformCacheService(redis);
|
|
||||||
const repo = new VehicleDataRepository();
|
|
||||||
const service = new VehicleDataService(cache, repo);
|
|
||||||
|
|
||||||
// Test getMakes
|
|
||||||
await service.getMakes(pool, 2024);
|
|
||||||
// First call: fetches from DB, caches result
|
|
||||||
// Second call: returns from cache
|
|
||||||
await service.getMakes(pool, 2024);
|
|
||||||
|
|
||||||
// Test getModels with string parameter
|
|
||||||
await service.getModels(pool, 2024, 'Ford');
|
|
||||||
|
|
||||||
// Test getTrims
|
|
||||||
await service.getTrims(pool, 2024, 'Ford', 'F-150');
|
|
||||||
|
|
||||||
// Test getEngines
|
|
||||||
await service.getEngines(pool, 2024, 'Ford', 'F-150', 'XLT');
|
|
||||||
|
|
||||||
// Test getTransmissions (NEW)
|
|
||||||
await service.getTransmissions(pool, 2024, 'Ford', 'F-150');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion:
|
|
||||||
|
|
||||||
- [ ] All method signatures updated (string parameters, not IDs)
|
|
||||||
- [ ] Return types changed to string[] (removed object types)
|
|
||||||
- [ ] getMakes() updated
|
|
||||||
- [ ] getModels() updated with make parameter
|
|
||||||
- [ ] getTrims() updated with make and model parameters
|
|
||||||
- [ ] getEngines() updated with make, model, trim parameters
|
|
||||||
- [ ] getTransmissions() method added (new)
|
|
||||||
- [ ] Cache service signatures updated
|
|
||||||
- [ ] Cache keys use strings (not IDs)
|
|
||||||
- [ ] Unit tests pass
|
|
||||||
- [ ] Integration tests verify caching works
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] File documentation updated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: TypeScript error "Type 'string[]' is not assignable to type 'MakeItem[]'"
|
|
||||||
**Cause**: Cache service still using old types
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Update cache service method signatures (Step 7)
|
|
||||||
- Ensure cache methods return `string[] | null` not `MakeItem[] | null`
|
|
||||||
|
|
||||||
### Issue: Cache keys collision
|
|
||||||
**Cause**: String-based keys might collide with old ID-based keys if Redis not cleared
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Clear Redis cache
|
|
||||||
docker exec mvp-redis redis-cli FLUSHDB
|
|
||||||
|
|
||||||
# Or clear specific pattern
|
|
||||||
docker exec mvp-redis redis-cli KEYS "makes:*" | xargs docker exec mvp-redis redis-cli DEL
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Service tests fail with parameter mismatch
|
|
||||||
**Cause**: Tests still passing old ID parameters
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Update test calls to use strings: `service.getModels(pool, 2024, 'Ford')` not `service.getModels(pool, 2024, 1)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handoff to Agent 4
|
|
||||||
|
|
||||||
Once complete, provide this information:
|
|
||||||
|
|
||||||
### Updated Service Contract
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
```typescript
|
|
||||||
getYears(pool: Pool): Promise<number[]>
|
|
||||||
getMakes(pool: Pool, year: number): Promise<string[]>
|
|
||||||
getModels(pool: Pool, year: number, make: string): Promise<string[]>
|
|
||||||
getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]>
|
|
||||||
getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]>
|
|
||||||
getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Changes for Agent 4**:
|
|
||||||
- All dropdown methods return `string[]`
|
|
||||||
- Parameters use strings (make, model, trim) not IDs
|
|
||||||
- `getTransmissions()` is a new method
|
|
||||||
- Caching layer handles string-based keys
|
|
||||||
|
|
||||||
### Verification Command
|
|
||||||
```bash
|
|
||||||
# Agent 4 can verify service is ready:
|
|
||||||
cd backend && npm run build
|
|
||||||
# Should compile with no errors in vehicle-data.service.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 3 (Platform Service): COMPLETE
|
|
||||||
|
|
||||||
Files Modified:
|
|
||||||
- backend/src/features/platform/domain/vehicle-data.service.ts
|
|
||||||
- backend/src/features/platform/domain/platform-cache.service.ts
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Updated all method signatures to accept strings (not IDs)
|
|
||||||
- Changed return types to string[] (removed object types)
|
|
||||||
- Updated cache keys to use string-based parameters
|
|
||||||
- Added getTransmissions() method with caching
|
|
||||||
- All methods still use Redis caching (TTL unchanged)
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ TypeScript compiles successfully
|
|
||||||
✓ Unit tests pass
|
|
||||||
✓ Integration tests confirm caching works
|
|
||||||
✓ Cache keys use string parameters
|
|
||||||
|
|
||||||
Agent 4 (Vehicles API) can now update controllers to use new service signatures.
|
|
||||||
|
|
||||||
Breaking Changes for Agent 4:
|
|
||||||
- Service methods accept strings: make, model, trim (not makeId, modelId, trimId)
|
|
||||||
- Service methods return string[] (not {id, name}[])
|
|
||||||
- New method: getTransmissions()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,780 +0,0 @@
|
|||||||
# Backend Vehicles API Update - Agent 4
|
|
||||||
|
|
||||||
## Task: Update Vehicles API to use string-based parameters and responses
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agent 3 (Platform Service) must be complete
|
|
||||||
**Estimated Time**: 1-2 hours
|
|
||||||
**Assigned To**: Agent 4 (Vehicles API)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Update the vehicles controller and service to accept string parameters (make, model, trim) instead of IDs, and return string arrays instead of objects. This is the final backend change before frontend updates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify Agent 3 Completed
|
|
||||||
```bash
|
|
||||||
# Verify service compiles
|
|
||||||
cd backend && npm run build
|
|
||||||
|
|
||||||
# Should see no errors in vehicle-data.service.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
```
|
|
||||||
backend/src/features/vehicles/api/vehicles.controller.ts
|
|
||||||
backend/src/features/vehicles/domain/vehicles.service.ts
|
|
||||||
backend/src/features/vehicles/types/index.ts (query parameter types)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Implementation Analysis
|
|
||||||
|
|
||||||
### Controller
|
|
||||||
|
|
||||||
**File**: `backend/src/features/vehicles/api/vehicles.controller.ts`
|
|
||||||
|
|
||||||
**Current Query Parameters** (ID-based):
|
|
||||||
```typescript
|
|
||||||
getDropdownModels: { year: number; make_id: number }
|
|
||||||
getDropdownTrims: { year: number; make_id: number; model_id: number }
|
|
||||||
getDropdownEngines: { year: number; make_id: number; model_id: number; trim_id: number }
|
|
||||||
getDropdownTransmissions: { year: number; make_id: number; model_id: number }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service
|
|
||||||
|
|
||||||
**File**: `backend/src/features/vehicles/domain/vehicles.service.ts`
|
|
||||||
|
|
||||||
**Current Methods**:
|
|
||||||
```typescript
|
|
||||||
getDropdownMakes(year: number): Promise<{ id: number; name: string }[]>
|
|
||||||
getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]>
|
|
||||||
getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]>
|
|
||||||
getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]>
|
|
||||||
getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> // Hardcoded!
|
|
||||||
getDropdownYears(): Promise<number[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 1: Update Vehicles Service
|
|
||||||
|
|
||||||
### Step 1: Update getDropdownMakes()
|
|
||||||
|
|
||||||
**Current** (lines 165-171):
|
|
||||||
```typescript
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownMakes(year: number): Promise<string[]> {
|
|
||||||
const vehicleDataService = getVehicleDataService();
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
logger.info('Fetching dropdown makes via platform module', { year });
|
|
||||||
return vehicleDataService.getMakes(pool, year);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 165: Return type `Promise<{ id: number; name: string }[]>` → `Promise<string[]>`
|
|
||||||
- Logic unchanged (platform service now returns string[])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Update getDropdownModels()
|
|
||||||
|
|
||||||
**Current** (lines 173-179):
|
|
||||||
```typescript
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownModels(year: number, make: string): Promise<string[]> {
|
|
||||||
const vehicleDataService = getVehicleDataService();
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
logger.info('Fetching dropdown models via platform module', { year, make });
|
|
||||||
return vehicleDataService.getModels(pool, year, make);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 173: Parameter `makeId: number` → `make: string`
|
|
||||||
- Line 173: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 177: Logger uses `make` instead of `makeId`
|
|
||||||
- Line 178: Service call uses `make` instead of `makeId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Update getDropdownTrims()
|
|
||||||
|
|
||||||
**Current** (lines 197-203):
|
|
||||||
```typescript
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownTrims(year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
const vehicleDataService = getVehicleDataService();
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
logger.info('Fetching dropdown trims via platform module', { year, make, model });
|
|
||||||
return vehicleDataService.getTrims(pool, year, make, model);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 197: Parameters `makeId: number, modelId: number` → `make: string, model: string`
|
|
||||||
- Line 197: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 201: Logger uses `make, model` instead of `makeId, modelId`
|
|
||||||
- Line 202: Service call uses `make, model` instead of `modelId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4: Update getDropdownEngines()
|
|
||||||
|
|
||||||
**Current** (lines 189-195):
|
|
||||||
```typescript
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownEngines(year: number, make: string, model: string, trim: string): Promise<string[]> {
|
|
||||||
const vehicleDataService = getVehicleDataService();
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
logger.info('Fetching dropdown engines via platform module', { year, make, model, trim });
|
|
||||||
return vehicleDataService.getEngines(pool, year, make, model, trim);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 189: Parameters changed to strings: `make: string, model: string, trim: string`
|
|
||||||
- Line 189: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 193: Logger uses string names
|
|
||||||
- Line 194: Service call uses all string parameters
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5: Update getDropdownTransmissions() - CRITICAL
|
|
||||||
|
|
||||||
**Current** (lines 181-187):
|
|
||||||
```typescript
|
|
||||||
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' }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownTransmissions(year: number, make: string, model: string): Promise<string[]> {
|
|
||||||
const vehicleDataService = getVehicleDataService();
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
logger.info('Fetching dropdown transmissions via platform module', { year, make, model });
|
|
||||||
return vehicleDataService.getTransmissions(pool, year, make, model);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 181: Parameters changed from IDs to strings: `make: string, model: string`
|
|
||||||
- Line 181: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 182-186: **REMOVED** hardcoded static list
|
|
||||||
- Now calls platform service for real transmission data
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2: Update Vehicles Controller
|
|
||||||
|
|
||||||
### Step 6: Update getDropdownMakes()
|
|
||||||
|
|
||||||
**Current** (lines 153-172):
|
|
||||||
```typescript
|
|
||||||
async getDropdownMakes(request: FastifyRequest<{ Querystring: { year: number } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year } = request.query;
|
|
||||||
if (!year || year < 1980 || year > new Date().getFullYear() + 1) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year parameter is required (1980-' + (new Date().getFullYear() + 1) + ')'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const makes = await this.vehiclesService.getDropdownMakes(year);
|
|
||||||
return reply.code(200).send(makes);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown makes', { error, year: request.query?.year });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get makes'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**: (Unchanged - already returns array, validation unchanged)
|
|
||||||
|
|
||||||
**Note**: No changes needed - query parameter is already just `year`, and service now returns `string[]` which gets passed through directly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 7: Update getDropdownModels()
|
|
||||||
|
|
||||||
**Current** (lines 174-193):
|
|
||||||
```typescript
|
|
||||||
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make_id: number } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make_id } = request.query;
|
|
||||||
if (!year || !make_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year and make_id parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = await this.vehiclesService.getDropdownModels(year, make_id);
|
|
||||||
return reply.code(200).send(models);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown models', { error, year: request.query?.year, make_id: request.query?.make_id });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get models'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make: string } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make } = request.query;
|
|
||||||
if (!year || !make || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year and make parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = await this.vehiclesService.getDropdownModels(year, make);
|
|
||||||
return reply.code(200).send(models);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown models', { error, year: request.query?.year, make: request.query?.make });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get models'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 174: Querystring type `make_id: number` → `make: string`
|
|
||||||
- Line 176: Destructure `make` instead of `make_id`
|
|
||||||
- Line 177: Validation changed from `make_id < 1` to `make.trim().length === 0`
|
|
||||||
- Line 180: Error message updated
|
|
||||||
- Line 184: Service call uses `make` string
|
|
||||||
- Line 187: Logger uses `make`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 8: Update getDropdownTrims()
|
|
||||||
|
|
||||||
**Current** (lines 237-256):
|
|
||||||
```typescript
|
|
||||||
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make_id, model_id } = request.query;
|
|
||||||
if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make_id, and model_id parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const trims = await this.vehiclesService.getDropdownTrims(year, make_id, model_id);
|
|
||||||
return reply.code(200).send(trims);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown trims', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get trims'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make, model } = request.query;
|
|
||||||
if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make, and model parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const trims = await this.vehiclesService.getDropdownTrims(year, make, model);
|
|
||||||
return reply.code(200).send(trims);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown trims', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get trims'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 237: Querystring types changed to `make: string; model: string`
|
|
||||||
- Line 239: Destructure string parameters
|
|
||||||
- Line 240: Validation uses `.trim().length === 0` instead of `< 1`
|
|
||||||
- Line 243: Error message updated
|
|
||||||
- Line 247: Service call uses strings
|
|
||||||
- Line 250: Logger uses strings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 9: Update getDropdownEngines()
|
|
||||||
|
|
||||||
**Current** (lines 216-235):
|
|
||||||
```typescript
|
|
||||||
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number; trim_id: number } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make_id, model_id, trim_id } = request.query;
|
|
||||||
if (!year || !make_id || !model_id || !trim_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1 || trim_id < 1) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make_id, model_id, and trim_id parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const engines = await this.vehiclesService.getDropdownEngines(year, make_id, model_id, trim_id);
|
|
||||||
return reply.code(200).send(engines);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown engines', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id, trim_id: request.query?.trim_id });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get engines'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make, model, trim } = request.query;
|
|
||||||
if (!year || !make || !model || !trim || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make, model, and trim parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const engines = await this.vehiclesService.getDropdownEngines(year, make, model, trim);
|
|
||||||
return reply.code(200).send(engines);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown engines', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model, trim: request.query?.trim });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get engines'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 216: Querystring types changed to strings
|
|
||||||
- Line 218: Destructure string parameters
|
|
||||||
- Line 219: Validation uses `.trim().length === 0`
|
|
||||||
- Line 222: Error message updated
|
|
||||||
- Line 226: Service call uses strings
|
|
||||||
- Line 229: Logger uses strings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 10: Update getDropdownTransmissions()
|
|
||||||
|
|
||||||
**Current** (lines 195-214):
|
|
||||||
```typescript
|
|
||||||
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make_id, model_id } = request.query;
|
|
||||||
if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make_id, and model_id parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make_id, model_id);
|
|
||||||
return reply.code(200).send(transmissions);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get transmissions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
|
|
||||||
try {
|
|
||||||
const { year, make, model } = request.query;
|
|
||||||
if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
|
|
||||||
return reply.code(400).send({
|
|
||||||
error: 'Bad Request',
|
|
||||||
message: 'Valid year, make, and model parameters are required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make, model);
|
|
||||||
return reply.code(200).send(transmissions);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
|
|
||||||
return reply.code(500).send({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: 'Failed to get transmissions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 195: Querystring types changed to strings
|
|
||||||
- Line 197: Destructure string parameters
|
|
||||||
- Line 198: Validation uses `.trim().length === 0`
|
|
||||||
- Line 201: Error message updated
|
|
||||||
- Line 205: Service call uses strings (now fetches real data!)
|
|
||||||
- Line 208: Logger uses strings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Manual API Testing
|
|
||||||
|
|
||||||
Test each endpoint with the new string-based parameters:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Assuming backend is running on localhost:3000
|
|
||||||
|
|
||||||
# Test getMakes
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["Acura", "Audi", "BMW", "Ford", "Honda", ...]
|
|
||||||
|
|
||||||
# Test getModels (note: make parameter, not make_id)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Ford" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["Bronco", "Edge", "Escape", "Explorer", "F-150", "Mustang", ...]
|
|
||||||
|
|
||||||
# Test getTrims
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["King Ranch", "Lariat", "Limited", "Platinum", "XL", "XLT", ...]
|
|
||||||
|
|
||||||
# Test getEngines
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["V6 2.7L Turbo", "V6 3.5L Turbo", "V8 5.0L", ...]
|
|
||||||
|
|
||||||
# Test getTransmissions (should return REAL data now, not hardcoded)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]
|
|
||||||
|
|
||||||
# Test with Tesla (electric vehicle - should show N/A for engine)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Tesla&model=Model 3&trim=Long Range" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: ["N/A (Electric)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Error Handling
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test missing make parameter
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: 400 Bad Request
|
|
||||||
|
|
||||||
# Test empty make parameter
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: 400 Bad Request
|
|
||||||
|
|
||||||
# Test invalid year
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=1900" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
# Expected: 400 Bad Request
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
Create automated tests:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// backend/src/features/vehicles/tests/integration/dropdown-api.test.ts
|
|
||||||
|
|
||||||
describe('Dropdown API Endpoints', () => {
|
|
||||||
let app: FastifyInstance;
|
|
||||||
let authToken: string;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await createTestApp();
|
|
||||||
authToken = await getTestAuthToken();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /api/vehicles/dropdown/makes', () => {
|
|
||||||
it('should return string array of makes', async () => {
|
|
||||||
const response = await app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/vehicles/dropdown/makes?year=2024',
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
const makes = JSON.parse(response.body);
|
|
||||||
expect(Array.isArray(makes)).toBe(true);
|
|
||||||
expect(typeof makes[0]).toBe('string');
|
|
||||||
expect(makes).toContain('Ford');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /api/vehicles/dropdown/models', () => {
|
|
||||||
it('should accept make string parameter', async () => {
|
|
||||||
const response = await app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/vehicles/dropdown/models?year=2024&make=Ford',
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
const models = JSON.parse(response.body);
|
|
||||||
expect(Array.isArray(models)).toBe(true);
|
|
||||||
expect(typeof models[0]).toBe('string');
|
|
||||||
expect(models).toContain('F-150');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return 400 for missing make', async () => {
|
|
||||||
const response = await app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/vehicles/dropdown/models?year=2024',
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(400);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /api/vehicles/dropdown/transmissions', () => {
|
|
||||||
it('should return real transmission data (not hardcoded)', async () => {
|
|
||||||
const response = await app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150',
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
const transmissions = JSON.parse(response.body);
|
|
||||||
expect(Array.isArray(transmissions)).toBe(true);
|
|
||||||
expect(transmissions.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Should NOT be the old hardcoded values
|
|
||||||
const hasDetailedTransmissions = transmissions.some((t: string) =>
|
|
||||||
t.includes('Speed') || t.includes('CVT')
|
|
||||||
);
|
|
||||||
expect(hasDetailedTransmissions).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion:
|
|
||||||
|
|
||||||
- [ ] Service methods updated (all use string parameters)
|
|
||||||
- [ ] Service return types changed to string[]
|
|
||||||
- [ ] getDropdownTransmissions() now fetches real data (not hardcoded)
|
|
||||||
- [ ] Controller query parameter types updated (make, model, trim not IDs)
|
|
||||||
- [ ] Controller validation updated (string length checks)
|
|
||||||
- [ ] Controller error messages updated
|
|
||||||
- [ ] All manual API tests pass
|
|
||||||
- [ ] Integration tests pass
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] Backend builds successfully: `npm run build`
|
|
||||||
- [ ] No console errors in logs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: "Cannot read property 'trim' of undefined"
|
|
||||||
**Cause**: Query parameters might be undefined if not provided
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```typescript
|
|
||||||
// Add extra validation before .trim()
|
|
||||||
if (!make || typeof make !== 'string' || make.trim().length === 0) {
|
|
||||||
return reply.code(400).send({ ... });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Transmissions still returning ["Automatic", "Manual"]
|
|
||||||
**Cause**: Service method not updated or cache not cleared
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Clear Redis cache
|
|
||||||
docker exec mvp-redis redis-cli FLUSHDB
|
|
||||||
|
|
||||||
# Restart backend
|
|
||||||
make rebuild
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Frontend still sending make_id parameter
|
|
||||||
**Cause**: Frontend not yet updated (Agent 5 not complete)
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- This is expected - Agent 5 will update frontend API client
|
|
||||||
- For now, test with curl using string parameters
|
|
||||||
- Document breaking changes for Agent 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Contract for Agent 5
|
|
||||||
|
|
||||||
Once complete, provide this to Agent 5 (Frontend API Client):
|
|
||||||
|
|
||||||
### New API Endpoints
|
|
||||||
|
|
||||||
**All endpoints now use string parameters (not IDs):**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Makes
|
|
||||||
GET /api/vehicles/dropdown/makes?year=2024
|
|
||||||
Response: string[] // ["Ford", "Honda", ...]
|
|
||||||
|
|
||||||
// Models
|
|
||||||
GET /api/vehicles/dropdown/models?year=2024&make=Ford
|
|
||||||
Response: string[] // ["F-150", "Mustang", ...]
|
|
||||||
|
|
||||||
// Trims
|
|
||||||
GET /api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150
|
|
||||||
Response: string[] // ["XLT", "Lariat", ...]
|
|
||||||
|
|
||||||
// Engines
|
|
||||||
GET /api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT
|
|
||||||
Response: string[] // ["V8 5.0L", ...]
|
|
||||||
|
|
||||||
// Transmissions
|
|
||||||
GET /api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150
|
|
||||||
Response: string[] // ["10-Speed Automatic", ...]
|
|
||||||
|
|
||||||
// Years (unchanged)
|
|
||||||
GET /api/vehicles/dropdown/years
|
|
||||||
Response: number[] // [2026, 2025, ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
**Query Parameters**:
|
|
||||||
- `make_id` → `make` (string)
|
|
||||||
- `model_id` → `model` (string)
|
|
||||||
- `trim_id` → `trim` (string)
|
|
||||||
|
|
||||||
**Response Format**:
|
|
||||||
- Old: `[{id: 1, name: "Ford"}, ...]`
|
|
||||||
- New: `["Ford", ...]`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 4 (Vehicles API): COMPLETE
|
|
||||||
|
|
||||||
Files Modified:
|
|
||||||
- backend/src/features/vehicles/api/vehicles.controller.ts
|
|
||||||
- backend/src/features/vehicles/domain/vehicles.service.ts
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Updated all controller query parameters to use strings (make, model, trim)
|
|
||||||
- Updated all service methods to accept string parameters
|
|
||||||
- Changed return types to string[] (removed objects)
|
|
||||||
- getDropdownTransmissions() now fetches real data (not hardcoded!)
|
|
||||||
- Updated validation to check string lengths
|
|
||||||
- Updated error messages to reflect new parameter names
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ TypeScript compiles successfully
|
|
||||||
✓ Backend builds successfully
|
|
||||||
✓ Manual API tests with curl pass
|
|
||||||
✓ Integration tests pass
|
|
||||||
✓ Transmissions return real data (verified)
|
|
||||||
✓ Electric vehicles show 'N/A (Electric)' for engines
|
|
||||||
|
|
||||||
Agent 5 (Frontend API Client) can now update frontend to use new API contract.
|
|
||||||
|
|
||||||
Breaking Changes for Agent 5:
|
|
||||||
- Query parameters changed: make_id→make, model_id→model, trim_id→trim
|
|
||||||
- Response format changed: {id, name}[] → string[]
|
|
||||||
- Cascade queries now use selected string values (not IDs)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,540 +0,0 @@
|
|||||||
# Vehicle Dropdown Database Migration - Master Overview
|
|
||||||
|
|
||||||
## Status: READY FOR IMPLEMENTATION
|
|
||||||
|
|
||||||
This document coordinates the migration from normalized ID-based vehicle dropdowns to a denormalized string-based system using ETL-generated data (1.1M+ vehicle options).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Context
|
|
||||||
|
|
||||||
### What Changed
|
|
||||||
- **Old System**: Normalized database (vehicles.make, vehicles.model, etc.) with ID-based API responses
|
|
||||||
- **New System**: Denormalized vehicle_options table with string-based API responses
|
|
||||||
- **Impact**: Complete replacement of vehicle dropdown system (database + backend + frontend)
|
|
||||||
|
|
||||||
### Why This Change
|
|
||||||
- **Better Data**: ETL pipeline provides 1.1M+ comprehensive vehicle configurations from 1980-2026
|
|
||||||
- **Simpler Queries**: Single denormalized table vs complex JOINs across 6+ tables
|
|
||||||
- **Real Transmissions**: 828 actual transmission types vs hardcoded ["Automatic", "Manual"]
|
|
||||||
- **Performance**: Composite indexes enable sub-50ms query times
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Strategy
|
|
||||||
|
|
||||||
### Parallel Work Units
|
|
||||||
|
|
||||||
This migration is broken into **8 independent work units** that can be executed by separate agents in parallel (where dependencies allow).
|
|
||||||
|
|
||||||
| Agent | Task | File | Dependencies | Can Start |
|
|
||||||
|-------|------|------|--------------|-----------|
|
|
||||||
| Agent 1 | Database Migration | `database-migration.md` | None | Immediately |
|
|
||||||
| Agent 2 | Platform Repository | `backend-platform-repository.md` | Agent 1 done | After DB |
|
|
||||||
| Agent 3 | Platform Service | `backend-platform-service.md` | Agent 2 done | After Repo |
|
|
||||||
| Agent 4 | Vehicles API | `backend-vehicles-api.md` | Agent 3 done | After Service |
|
|
||||||
| Agent 5 | Frontend API Client | `frontend-api-client.md` | Agent 4 API contract defined | After API contract |
|
|
||||||
| Agent 6 | Frontend Forms | `frontend-vehicle-form.md` | Agent 5 done | After API client |
|
|
||||||
| Agent 7 | Testing | `testing-validation.md` | Agents 1-6 done | After all |
|
|
||||||
| Agent 8 | VIN Decode (if needed) | Manual investigation | Agent 1 done | After DB |
|
|
||||||
|
|
||||||
### Critical Path
|
|
||||||
```
|
|
||||||
Database (Agent 1)
|
|
||||||
↓
|
|
||||||
Platform Repository (Agent 2)
|
|
||||||
↓
|
|
||||||
Platform Service (Agent 3)
|
|
||||||
↓
|
|
||||||
Vehicles API (Agent 4)
|
|
||||||
↓
|
|
||||||
Frontend API Client (Agent 5)
|
|
||||||
↓
|
|
||||||
Frontend Forms (Agent 6)
|
|
||||||
↓
|
|
||||||
Testing (Agent 7)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parallel opportunities:**
|
|
||||||
- Agent 8 can investigate VIN decode while Agents 2-6 work
|
|
||||||
- Agent 5 can start after Agent 4 defines API contract (doesn't need full implementation)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Design Decisions (FINAL)
|
|
||||||
|
|
||||||
### 1. API Format: String-Based
|
|
||||||
**Decision**: Switch from `{id: number, name: string}[]` to `string[]`
|
|
||||||
|
|
||||||
**Rationale**: Aligns with denormalized database design, simpler code
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
GET /api/vehicles/dropdown/makes?year=2024
|
|
||||||
Response: [{id: 1, name: "Ford"}, {id: 2, name: "Honda"}]
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
GET /api/vehicles/dropdown/makes?year=2024
|
|
||||||
Response: ["Ford", "Honda"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Migration Strategy: Complete Replacement
|
|
||||||
**Decision**: Replace vehicles.* schema entirely (not parallel schemas)
|
|
||||||
|
|
||||||
**Rationale**: Clean architecture, no data duplication, simpler maintenance
|
|
||||||
|
|
||||||
**Impact**: Must verify VIN decode still works (Agent 8)
|
|
||||||
|
|
||||||
### 3. NULL Handling: Show with 'N/A'
|
|
||||||
**Decision**: Include electric vehicles, display 'N/A' for missing engine/transmission
|
|
||||||
|
|
||||||
**Rationale**: Ensures all vehicles available, handles 1.1% of records with NULL engine_id
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```typescript
|
|
||||||
// Electric vehicle in dropdown
|
|
||||||
{
|
|
||||||
engine: "N/A (Electric)",
|
|
||||||
transmission: "Single-Speed Automatic"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Implementation Approach: Single Update
|
|
||||||
**Decision**: No feature flags, complete migration in one cycle
|
|
||||||
|
|
||||||
**Rationale**: Faster delivery, cleaner code, no dual-system complexity
|
|
||||||
|
|
||||||
**Requirement**: Thorough testing before deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
### API Changes
|
|
||||||
|
|
||||||
**Endpoint Response Format:**
|
|
||||||
```typescript
|
|
||||||
// All dropdown endpoints now return string[]
|
|
||||||
GET /api/vehicles/dropdown/years → number[] (unchanged)
|
|
||||||
GET /api/vehicles/dropdown/makes → string[] (was {id, name}[])
|
|
||||||
GET /api/vehicles/dropdown/models → string[] (was {id, name}[])
|
|
||||||
GET /api/vehicles/dropdown/trims → string[] (was {id, name}[])
|
|
||||||
GET /api/vehicles/dropdown/engines → string[] (was {id, name}[])
|
|
||||||
GET /api/vehicles/dropdown/transmissions → string[] (was {id, name}[])
|
|
||||||
```
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
```typescript
|
|
||||||
// Parameters now use string values, not IDs
|
|
||||||
GET /api/vehicles/dropdown/models?year=2024&make=Ford (was make_id=1)
|
|
||||||
GET /api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150 (was make_id=1&model_id=42)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Schema Changes
|
|
||||||
|
|
||||||
**Removed Tables:**
|
|
||||||
```sql
|
|
||||||
vehicles.make
|
|
||||||
vehicles.model
|
|
||||||
vehicles.model_year
|
|
||||||
vehicles.trim
|
|
||||||
vehicles.engine
|
|
||||||
vehicles.trim_engine
|
|
||||||
vehicles.transmission
|
|
||||||
vehicles.trim_transmission
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Tables:**
|
|
||||||
```sql
|
|
||||||
public.engines -- 30,066 records
|
|
||||||
public.transmissions -- 828 records
|
|
||||||
public.vehicle_options -- 1,122,644 records
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Database Functions:**
|
|
||||||
```sql
|
|
||||||
get_makes_for_year(year INT)
|
|
||||||
get_models_for_year_make(year INT, make VARCHAR)
|
|
||||||
get_trims_for_year_make_model(year INT, make VARCHAR, model VARCHAR)
|
|
||||||
get_options_for_vehicle(year INT, make VARCHAR, model VARCHAR, trim VARCHAR)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Type Changes
|
|
||||||
|
|
||||||
**Old Type:**
|
|
||||||
```typescript
|
|
||||||
interface DropdownOption {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New Type:**
|
|
||||||
```typescript
|
|
||||||
type DropdownOption = string;
|
|
||||||
// Or simply use string[] directly
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## New Database Schema Details
|
|
||||||
|
|
||||||
### Tables
|
|
||||||
|
|
||||||
**engines**
|
|
||||||
```sql
|
|
||||||
CREATE TABLE engines (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL -- e.g., "V8 5.0L", "L4 2.0L Turbo"
|
|
||||||
);
|
|
||||||
-- 30,066 records
|
|
||||||
```
|
|
||||||
|
|
||||||
**transmissions**
|
|
||||||
```sql
|
|
||||||
CREATE TABLE transmissions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
type VARCHAR(255) NOT NULL -- e.g., "8-Speed Automatic", "6-Speed Manual"
|
|
||||||
);
|
|
||||||
-- 828 records
|
|
||||||
```
|
|
||||||
|
|
||||||
**vehicle_options**
|
|
||||||
```sql
|
|
||||||
CREATE TABLE vehicle_options (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
year INTEGER NOT NULL,
|
|
||||||
make VARCHAR(100) NOT NULL, -- "Ford", "Honda" (Title Case)
|
|
||||||
model VARCHAR(100) NOT NULL, -- "F-150", "Civic"
|
|
||||||
trim VARCHAR(255), -- "XLT SuperCrew", "Sport Touring"
|
|
||||||
engine_id INTEGER REFERENCES engines(id),
|
|
||||||
transmission_id INTEGER REFERENCES transmissions(id)
|
|
||||||
);
|
|
||||||
-- 1,122,644 records
|
|
||||||
-- 1.1% have NULL engine_id (electric vehicles)
|
|
||||||
|
|
||||||
-- Composite indexes for cascade queries
|
|
||||||
CREATE INDEX idx_vehicle_year_make ON vehicle_options(year, make);
|
|
||||||
CREATE INDEX idx_vehicle_year_make_model ON vehicle_options(year, make, model);
|
|
||||||
CREATE INDEX idx_vehicle_year_make_model_trim ON vehicle_options(year, make, model, trim);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Functions
|
|
||||||
|
|
||||||
These functions optimize common queries:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Returns distinct makes for a given year
|
|
||||||
get_makes_for_year(year INT) RETURNS TABLE(make VARCHAR)
|
|
||||||
|
|
||||||
-- Returns distinct models for year/make
|
|
||||||
get_models_for_year_make(year INT, make VARCHAR) RETURNS TABLE(model VARCHAR)
|
|
||||||
|
|
||||||
-- Returns distinct trims for year/make/model
|
|
||||||
get_trims_for_year_make_model(year INT, make VARCHAR, model VARCHAR)
|
|
||||||
RETURNS TABLE(trim_name VARCHAR)
|
|
||||||
|
|
||||||
-- Returns engine/transmission options for specific vehicle
|
|
||||||
get_options_for_vehicle(year INT, make VARCHAR, model VARCHAR, trim VARCHAR)
|
|
||||||
RETURNS TABLE(engine_name VARCHAR, transmission_type VARCHAR, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Flow Changes
|
|
||||||
|
|
||||||
### Old Flow (ID-Based Cascade)
|
|
||||||
```
|
|
||||||
1. User selects Year (2024)
|
|
||||||
→ GET /dropdown/makes?year=2024
|
|
||||||
→ Returns: [{id: 1, name: "Ford"}, {id: 2, name: "Honda"}]
|
|
||||||
|
|
||||||
2. User selects Make (clicks "Ford", id=1)
|
|
||||||
→ GET /dropdown/models?year=2024&make_id=1
|
|
||||||
→ Returns: [{id: 42, name: "F-150"}, {id: 43, name: "Mustang"}]
|
|
||||||
|
|
||||||
3. User selects Model (clicks "F-150", id=42)
|
|
||||||
→ GET /dropdown/trims?year=2024&make_id=1&model_id=42
|
|
||||||
→ Returns: [{id: 301, name: "XLT"}, {id: 302, name: "Lariat"}]
|
|
||||||
|
|
||||||
4. Form stores: make="Ford", model="F-150", trim="XLT" (name strings)
|
|
||||||
```
|
|
||||||
|
|
||||||
### New Flow (String-Based Cascade)
|
|
||||||
```
|
|
||||||
1. User selects Year (2024)
|
|
||||||
→ GET /dropdown/makes?year=2024
|
|
||||||
→ Returns: ["Ford", "Honda"]
|
|
||||||
|
|
||||||
2. User selects Make ("Ford")
|
|
||||||
→ GET /dropdown/models?year=2024&make=Ford
|
|
||||||
→ Returns: ["F-150", "Mustang"]
|
|
||||||
|
|
||||||
3. User selects Model ("F-150")
|
|
||||||
→ GET /dropdown/trims?year=2024&make=Ford&model=F-150
|
|
||||||
→ Returns: ["XLT", "Lariat"]
|
|
||||||
|
|
||||||
4. Form stores: make="Ford", model="F-150", trim="XLT" (same strings)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefit**: Simpler frontend logic - no ID tracking, direct string values
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Locations Reference
|
|
||||||
|
|
||||||
### Database Files
|
|
||||||
```
|
|
||||||
data/make-model-import/migrations/001_create_vehicle_database.sql
|
|
||||||
data/make-model-import/output/01_engines.sql
|
|
||||||
data/make-model-import/output/02_transmissions.sql
|
|
||||||
data/make-model-import/output/03_vehicle_options.sql
|
|
||||||
data/make-model-import/IMPLEMENTATION_SUMMARY.md (reference doc)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Platform Feature
|
|
||||||
```
|
|
||||||
backend/src/features/platform/domain/vehicle-data.repository.ts
|
|
||||||
backend/src/features/platform/domain/vehicle-data.service.ts
|
|
||||||
backend/src/features/platform/types/index.ts
|
|
||||||
backend/src/features/platform/cache/platform-cache.service.ts (may need updates)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Vehicles Feature
|
|
||||||
```
|
|
||||||
backend/src/features/vehicles/api/vehicles.controller.ts
|
|
||||||
backend/src/features/vehicles/api/vehicles.routes.ts (minimal changes)
|
|
||||||
backend/src/features/vehicles/domain/vehicles.service.ts
|
|
||||||
backend/src/features/vehicles/types/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
```
|
|
||||||
frontend/src/features/vehicles/api/vehicles.api.ts
|
|
||||||
frontend/src/features/vehicles/types/vehicles.types.ts
|
|
||||||
frontend/src/features/vehicles/components/VehicleForm.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
### Technical Requirements
|
|
||||||
- [ ] Database migration completes successfully (3 SQL files imported)
|
|
||||||
- [ ] All dropdown endpoints return string[] format
|
|
||||||
- [ ] Query parameters use strings (not IDs)
|
|
||||||
- [ ] NULL values display as 'N/A' or appropriate label
|
|
||||||
- [ ] Transmissions show real data (not hardcoded)
|
|
||||||
- [ ] Composite indexes perform sub-50ms queries
|
|
||||||
- [ ] All backend tests pass
|
|
||||||
- [ ] All frontend tests pass
|
|
||||||
- [ ] Mobile responsiveness verified
|
|
||||||
|
|
||||||
### Functional Requirements
|
|
||||||
- [ ] Create vehicle form works end-to-end
|
|
||||||
- [ ] Edit vehicle form loads and displays correctly
|
|
||||||
- [ ] Cascading dropdowns work: Year → Make → Model → Trim → Engine/Trans
|
|
||||||
- [ ] Electric vehicles appear in results with 'N/A' engine display
|
|
||||||
- [ ] Saved vehicles store correct string values
|
|
||||||
- [ ] VIN decode feature still operational (if dependent on schema)
|
|
||||||
|
|
||||||
### Quality Requirements (per CLAUDE.md)
|
|
||||||
- [ ] All linters pass with zero issues
|
|
||||||
- [ ] All automated checks green
|
|
||||||
- [ ] No formatting errors
|
|
||||||
- [ ] Code follows project conventions
|
|
||||||
- [ ] Old code deleted (not commented out)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
### Backend API Tests
|
|
||||||
- [ ] GET /dropdown/years returns number[]
|
|
||||||
- [ ] GET /dropdown/makes?year=2024 returns string[]
|
|
||||||
- [ ] GET /dropdown/models?year=2024&make=Ford returns string[]
|
|
||||||
- [ ] GET /dropdown/trims with all params returns string[]
|
|
||||||
- [ ] GET /dropdown/engines returns string[] with 'N/A' for electric
|
|
||||||
- [ ] GET /dropdown/transmissions returns real data (not hardcoded)
|
|
||||||
- [ ] Invalid parameters return 400 errors
|
|
||||||
- [ ] Database errors return 500 with appropriate message
|
|
||||||
|
|
||||||
### Frontend Form Tests
|
|
||||||
- [ ] Create form: Year dropdown loads on mount
|
|
||||||
- [ ] Create form: Make dropdown loads after year selected
|
|
||||||
- [ ] Create form: Model dropdown loads after make selected
|
|
||||||
- [ ] Create form: Trim dropdown loads after model selected
|
|
||||||
- [ ] Create form: Engine dropdown loads after trim selected
|
|
||||||
- [ ] Create form: Submission saves correct string values
|
|
||||||
- [ ] Edit form: Loads existing vehicle data correctly
|
|
||||||
- [ ] Edit form: Pre-populates all dropdowns in correct order
|
|
||||||
- [ ] Edit form: Allows changing selections and cascades properly
|
|
||||||
|
|
||||||
### Mobile Testing (REQUIRED per CLAUDE.md)
|
|
||||||
- [ ] Dropdowns render correctly on mobile viewport
|
|
||||||
- [ ] Touch interactions work smoothly
|
|
||||||
- [ ] Form is usable on small screens
|
|
||||||
- [ ] No horizontal scrolling issues
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- [ ] Complete flow: Select all fields → Save → Verify in DB
|
|
||||||
- [ ] Edit existing vehicle → Change year → Cascades reset correctly
|
|
||||||
- [ ] Electric vehicle selection → Engine shows 'N/A' → Saves correctly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If critical issues discovered:
|
|
||||||
|
|
||||||
1. **Database Rollback**
|
|
||||||
```bash
|
|
||||||
# Drop new tables
|
|
||||||
DROP TABLE vehicle_options CASCADE;
|
|
||||||
DROP TABLE transmissions CASCADE;
|
|
||||||
DROP TABLE engines CASCADE;
|
|
||||||
|
|
||||||
# Restore vehicles.* schema from backup
|
|
||||||
# (ensure backup created before migration)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Code Rollback**
|
|
||||||
```bash
|
|
||||||
git revert <commit-hash> # Revert all changes
|
|
||||||
make rebuild # Rebuild containers
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Partial Rollback (if only frontend broken)**
|
|
||||||
- Revert frontend changes only
|
|
||||||
- Backend can stay with new system
|
|
||||||
- Fix frontend issues and redeploy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues & Solutions
|
|
||||||
|
|
||||||
### Issue: VIN Decode Broken
|
|
||||||
**Symptom**: VIN decode endpoint returns errors after migration
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Check if VIN decode queries vehicles.* tables
|
|
||||||
- If yes, need to create separate VIN decode data solution
|
|
||||||
- May need to keep minimal vehicles.* for VIN decode only
|
|
||||||
|
|
||||||
### Issue: Electric Vehicles Show No Data
|
|
||||||
**Symptom**: Electric vehicles missing from dropdowns
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Verify NULL handling in repository queries
|
|
||||||
- Check that 'N/A' label generation works correctly
|
|
||||||
- Ensure frontend displays 'N/A' properly
|
|
||||||
|
|
||||||
### Issue: Slow Query Performance
|
|
||||||
**Symptom**: Dropdown queries take > 100ms
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Verify composite indexes created: `\d vehicle_options`
|
|
||||||
- Check query plans: `EXPLAIN ANALYZE <query>`
|
|
||||||
- Ensure using database functions (not raw queries)
|
|
||||||
- Consider adding Redis caching
|
|
||||||
|
|
||||||
### Issue: Make Names in Wrong Case
|
|
||||||
**Symptom**: Makes show as "FORD" instead of "Ford"
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Verify ETL conversion to Title Case worked
|
|
||||||
- Check database: `SELECT DISTINCT make FROM vehicle_options LIMIT 10;`
|
|
||||||
- If wrong, re-run ETL with case conversion fix
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent Communication Protocol
|
|
||||||
|
|
||||||
### Completion Signals
|
|
||||||
|
|
||||||
Each agent should:
|
|
||||||
1. Update their assigned documentation file with actual changes made
|
|
||||||
2. Post completion status in project communication channel
|
|
||||||
3. List any deviations from plan
|
|
||||||
4. Note any blockers for dependent agents
|
|
||||||
|
|
||||||
### Handoff Information
|
|
||||||
|
|
||||||
When completing work, agents should document:
|
|
||||||
- **What changed**: Specific files and functions modified
|
|
||||||
- **How to verify**: Command or test to confirm work complete
|
|
||||||
- **Breaking changes**: Any API changes affecting downstream agents
|
|
||||||
- **Blockers resolved**: Issues fixed that were blocking others
|
|
||||||
|
|
||||||
### Example Completion Message
|
|
||||||
```
|
|
||||||
Agent 2 (Platform Repository): COMPLETE
|
|
||||||
|
|
||||||
Files modified:
|
|
||||||
- backend/src/features/platform/domain/vehicle-data.repository.ts
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
- getMakes() now returns string[] using get_makes_for_year()
|
|
||||||
- All 5 dropdown methods updated to query vehicle_options table
|
|
||||||
- NULL engine_id returns 'N/A (Electric)' label
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
- Run: npm test -- vehicle-data.repository.test.ts
|
|
||||||
- All tests passing
|
|
||||||
|
|
||||||
API Contract for Agent 3:
|
|
||||||
- getMakes(pool, year) → Promise<string[]>
|
|
||||||
- getModels(pool, year, make) → Promise<string[]>
|
|
||||||
- getTrims(pool, year, make, model) → Promise<string[]>
|
|
||||||
- getEngines(pool, year, make, model, trim) → Promise<string[]>
|
|
||||||
- getTransmissions(pool, year, make, model) → Promise<string[]>
|
|
||||||
|
|
||||||
Agent 3 can now proceed with service layer updates.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detailed Task Documents
|
|
||||||
|
|
||||||
Each task has a dedicated document with implementation details:
|
|
||||||
|
|
||||||
1. **[Database Migration](./database-migration.md)** - Agent 1
|
|
||||||
2. **[Platform Repository Updates](./backend-platform-repository.md)** - Agent 2
|
|
||||||
3. **[Platform Service Updates](./backend-platform-service.md)** - Agent 3
|
|
||||||
4. **[Vehicles API Updates](./backend-vehicles-api.md)** - Agent 4
|
|
||||||
5. **[Frontend API Client Updates](./frontend-api-client.md)** - Agent 5
|
|
||||||
6. **[Frontend Form Updates](./frontend-vehicle-form.md)** - Agent 6
|
|
||||||
7. **[Testing & Validation](./testing-validation.md)** - Agent 7
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions or Issues
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
1. Check this master doc for common issues
|
|
||||||
2. Review your task-specific document
|
|
||||||
3. Check database state: `docker exec mvp-postgres psql -U postgres -d motovaultpro`
|
|
||||||
4. Check logs: `make logs`
|
|
||||||
5. Consult original data docs: `data/make-model-import/IMPLEMENTATION_SUMMARY.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline Estimate
|
|
||||||
|
|
||||||
- **Agent 1** (Database): 30 minutes
|
|
||||||
- **Agent 2** (Platform Repo): 1-2 hours
|
|
||||||
- **Agent 3** (Platform Service): 1 hour
|
|
||||||
- **Agent 4** (Vehicles API): 1-2 hours
|
|
||||||
- **Agent 5** (Frontend API): 1 hour
|
|
||||||
- **Agent 6** (Frontend Forms): 2-3 hours
|
|
||||||
- **Agent 7** (Testing): 2-3 hours
|
|
||||||
|
|
||||||
**Total Sequential**: ~10-14 hours
|
|
||||||
**With Parallelization**: ~6-8 hours (agents work simultaneously where possible)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
# Database Migration Guide - Agent 1
|
|
||||||
|
|
||||||
## Task: Replace vehicles.* schema with new ETL-generated database
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: None (can start immediately)
|
|
||||||
**Estimated Time**: 30 minutes
|
|
||||||
**Assigned To**: Agent 1 (Database)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Replace the normalized vehicles.* schema with a denormalized vehicle_options table populated from ETL-generated data (1.1M+ records from 1980-2026).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Required Files
|
|
||||||
All files are already present in the repository:
|
|
||||||
|
|
||||||
```
|
|
||||||
data/make-model-import/migrations/001_create_vehicle_database.sql
|
|
||||||
data/make-model-import/output/01_engines.sql
|
|
||||||
data/make-model-import/output/02_transmissions.sql
|
|
||||||
data/make-model-import/output/03_vehicle_options.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Access
|
|
||||||
```bash
|
|
||||||
# Verify Docker container is running
|
|
||||||
docker ps | grep mvp-postgres
|
|
||||||
|
|
||||||
# Access PostgreSQL
|
|
||||||
docker exec -it mvp-postgres psql -U postgres -d motovaultpro
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Backup Current Schema (Safety)
|
|
||||||
|
|
||||||
Before making any changes, backup the existing vehicles.* schema:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create backup directory
|
|
||||||
mkdir -p data/backups
|
|
||||||
|
|
||||||
# Dump vehicles schema only
|
|
||||||
docker exec mvp-postgres pg_dump -U postgres -d motovaultpro \
|
|
||||||
--schema=vehicles \
|
|
||||||
--format=plain \
|
|
||||||
--file=/tmp/vehicles_schema_backup.sql
|
|
||||||
|
|
||||||
# Copy backup to host
|
|
||||||
docker cp mvp-postgres:/tmp/vehicles_schema_backup.sql \
|
|
||||||
data/backups/vehicles_schema_backup_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
|
|
||||||
# Verify backup exists
|
|
||||||
ls -lh data/backups/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification**: Backup file should be 100KB-1MB in size
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2: Drop Existing vehicles.* Tables
|
|
||||||
|
|
||||||
Drop all normalized tables in the vehicles schema:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -it mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
-- Drop tables in correct order (respect foreign keys)
|
|
||||||
DROP TABLE IF EXISTS vehicles.trim_transmission CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.trim_engine CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.transmission CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.engine CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.trim CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.model_year CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.model CASCADE;
|
|
||||||
DROP TABLE IF EXISTS vehicles.make CASCADE;
|
|
||||||
|
|
||||||
-- Drop views if they exist
|
|
||||||
DROP VIEW IF EXISTS vehicles.available_years CASCADE;
|
|
||||||
DROP VIEW IF EXISTS vehicles.makes_by_year CASCADE;
|
|
||||||
DROP VIEW IF EXISTS vehicles.models_by_year_make CASCADE;
|
|
||||||
|
|
||||||
-- Optionally drop the entire schema
|
|
||||||
-- DROP SCHEMA IF EXISTS vehicles CASCADE;
|
|
||||||
|
|
||||||
-- Verify all tables dropped
|
|
||||||
SELECT table_name FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'vehicles';
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification**: Query should return 0 rows (no tables left in vehicles schema)
|
|
||||||
|
|
||||||
**Note**: This is a destructive operation. Ensure backup completed successfully before proceeding.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Run New Migration
|
|
||||||
|
|
||||||
Execute the new schema migration that creates:
|
|
||||||
- `engines` table
|
|
||||||
- `transmissions` table
|
|
||||||
- `vehicle_options` table
|
|
||||||
- Database functions for cascade queries
|
|
||||||
- Composite indexes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run migration SQL
|
|
||||||
docker exec -i mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
< data/make-model-import/migrations/001_create_vehicle_database.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification**: Check for error messages. Successful output should include:
|
|
||||||
```
|
|
||||||
CREATE TABLE
|
|
||||||
CREATE TABLE
|
|
||||||
CREATE TABLE
|
|
||||||
CREATE INDEX
|
|
||||||
CREATE INDEX
|
|
||||||
CREATE FUNCTION
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Verify Schema Created
|
|
||||||
|
|
||||||
Check that all tables and functions were created successfully:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -it mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
-- List all tables
|
|
||||||
\dt
|
|
||||||
|
|
||||||
-- Describe engines table
|
|
||||||
\d engines
|
|
||||||
|
|
||||||
-- Describe transmissions table
|
|
||||||
\d transmissions
|
|
||||||
|
|
||||||
-- Describe vehicle_options table
|
|
||||||
\d vehicle_options
|
|
||||||
|
|
||||||
-- List indexes on vehicle_options
|
|
||||||
\di vehicle_options*
|
|
||||||
|
|
||||||
-- List functions
|
|
||||||
\df get_makes_for_year
|
|
||||||
\df get_models_for_year_make
|
|
||||||
\df get_trims_for_year_make_model
|
|
||||||
\df get_options_for_vehicle
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Output**:
|
|
||||||
- 3 tables: `engines`, `transmissions`, `vehicle_options`
|
|
||||||
- Indexes: `idx_vehicle_year_make`, `idx_vehicle_year_make_model`, `idx_vehicle_year_make_model_trim`
|
|
||||||
- 4 database functions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5: Import Engines Data
|
|
||||||
|
|
||||||
Import 30,066 engine records:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
< data/make-model-import/output/01_engines.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM engines;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: 30,066 rows
|
|
||||||
|
|
||||||
**Sample Data Check**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT id, name FROM engines LIMIT 10;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Format**: Names like "V8 5.0L", "L4 2.0L Turbo", "V6 3.5L"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6: Import Transmissions Data
|
|
||||||
|
|
||||||
Import 828 transmission records:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
< data/make-model-import/output/02_transmissions.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM transmissions;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: 828 rows
|
|
||||||
|
|
||||||
**Sample Data Check**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT id, type FROM transmissions LIMIT 10;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Format**: Types like "8-Speed Automatic", "6-Speed Manual", "CVT"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7: Import Vehicle Options Data
|
|
||||||
|
|
||||||
Import 1,122,644 vehicle option records (this may take 2-5 minutes):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
< data/make-model-import/output/03_vehicle_options.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: This is the largest import (51MB SQL file). You should see periodic output as batches are inserted.
|
|
||||||
|
|
||||||
**Verification**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: 1,122,644 rows
|
|
||||||
|
|
||||||
**Sample Data Check**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT year, make, model, trim FROM vehicle_options LIMIT 10;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Data like:
|
|
||||||
```
|
|
||||||
year | make | model | trim
|
|
||||||
------+---------+---------+---------------
|
|
||||||
2024 | Ford | F-150 | XLT SuperCrew
|
|
||||||
2024 | Honda | Civic | Sport Touring
|
|
||||||
2023 | Toyota | Camry | SE
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 8: Verify Data Quality
|
|
||||||
|
|
||||||
Run quality checks on imported data:
|
|
||||||
|
|
||||||
### Check Year Range
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT
|
|
||||||
MIN(year) as min_year,
|
|
||||||
MAX(year) as max_year,
|
|
||||||
COUNT(DISTINCT year) as total_years
|
|
||||||
FROM vehicle_options;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: min_year=1980, max_year=2026, total_years=47
|
|
||||||
|
|
||||||
### Check Make Count
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(DISTINCT make) FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: 53 makes
|
|
||||||
|
|
||||||
### Check NULL Engine IDs (Electric Vehicles)
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_records,
|
|
||||||
COUNT(*) FILTER (WHERE engine_id IS NULL) as null_engines,
|
|
||||||
ROUND(100.0 * COUNT(*) FILTER (WHERE engine_id IS NULL) / COUNT(*), 2) as null_percentage
|
|
||||||
FROM vehicle_options;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: ~1.1% NULL engine_id (approximately 11,951 records)
|
|
||||||
|
|
||||||
### Sample Electric Vehicle Data
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT year, make, model, trim, engine_id, transmission_id
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE engine_id IS NULL
|
|
||||||
LIMIT 10;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Should see Tesla, Lucid, Rivian, or other electric vehicles with NULL engine_id
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 9: Test Database Functions
|
|
||||||
|
|
||||||
Test the cascade query functions:
|
|
||||||
|
|
||||||
### Test 1: Get Makes for Year
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT * FROM get_makes_for_year(2024) LIMIT 10;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Returns string list of makes: "Ford", "Honda", "Toyota", etc.
|
|
||||||
|
|
||||||
### Test 2: Get Models for Year and Make
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT * FROM get_models_for_year_make(2024, 'Ford') LIMIT 10;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Returns Ford models: "F-150", "Mustang", "Explorer", etc.
|
|
||||||
|
|
||||||
### Test 3: Get Trims for Year, Make, Model
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT * FROM get_trims_for_year_make_model(2024, 'Ford', 'F-150') LIMIT 10;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Returns F-150 trims: "XLT", "Lariat", "King Ranch", etc.
|
|
||||||
|
|
||||||
### Test 4: Get Options for Vehicle
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT engine_name, transmission_type
|
|
||||||
FROM get_options_for_vehicle(2024, 'Ford', 'F-150', 'XLT')
|
|
||||||
LIMIT 10;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Returns engine/transmission combinations available for 2024 Ford F-150 XLT
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 10: Performance Validation
|
|
||||||
|
|
||||||
Verify query performance is sub-50ms as claimed:
|
|
||||||
|
|
||||||
### Test Index Usage
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
EXPLAIN ANALYZE
|
|
||||||
SELECT DISTINCT make
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE year = 2024;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Query plan should show index usage:
|
|
||||||
```
|
|
||||||
Index Scan using idx_vehicle_year_make ...
|
|
||||||
Execution Time: < 50 ms
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Cascade Query Performance
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
EXPLAIN ANALYZE
|
|
||||||
SELECT DISTINCT model
|
|
||||||
FROM vehicle_options
|
|
||||||
WHERE year = 2024 AND make = 'Ford';
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**: Should use composite index `idx_vehicle_year_make`, execution time < 50ms
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion, verify:
|
|
||||||
|
|
||||||
- [ ] Backup of old schema created successfully
|
|
||||||
- [ ] Old vehicles.* tables dropped
|
|
||||||
- [ ] New migration executed without errors
|
|
||||||
- [ ] Engines table has 30,066 records
|
|
||||||
- [ ] Transmissions table has 828 records
|
|
||||||
- [ ] Vehicle_options table has 1,122,644 records
|
|
||||||
- [ ] Year range is 1980-2026 (47 years)
|
|
||||||
- [ ] 53 distinct makes present
|
|
||||||
- [ ] ~1.1% of records have NULL engine_id
|
|
||||||
- [ ] All 4 database functions exist and return data
|
|
||||||
- [ ] Composite indexes created (3 indexes)
|
|
||||||
- [ ] Query performance is sub-50ms
|
|
||||||
- [ ] No error messages in PostgreSQL logs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error: "relation already exists"
|
|
||||||
**Cause**: Tables from old migration still present
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Drop tables explicitly
|
|
||||||
docker exec -it mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "DROP TABLE IF EXISTS vehicle_options CASCADE;"
|
|
||||||
# Then re-run migration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "duplicate key value violates unique constraint"
|
|
||||||
**Cause**: Data already imported, trying to import again
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Truncate tables
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
TRUNCATE TABLE vehicle_options CASCADE;
|
|
||||||
TRUNCATE TABLE engines CASCADE;
|
|
||||||
TRUNCATE TABLE transmissions CASCADE;
|
|
||||||
EOF
|
|
||||||
# Then re-import data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import Takes Too Long
|
|
||||||
**Symptom**: Import hangs or takes > 10 minutes
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
1. Check Docker resources (increase memory/CPU if needed)
|
|
||||||
2. Check disk space: `df -h`
|
|
||||||
3. Check PostgreSQL logs: `docker logs mvp-postgres`
|
|
||||||
4. Try importing in smaller batches (split SQL files if necessary)
|
|
||||||
|
|
||||||
### Performance Issues
|
|
||||||
**Symptom**: Queries take > 100ms
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Verify indexes were created
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "\di vehicle_options*"
|
|
||||||
|
|
||||||
# Analyze tables for query optimizer
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
ANALYZE engines;
|
|
||||||
ANALYZE transmissions;
|
|
||||||
ANALYZE vehicle_options;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback Procedure
|
|
||||||
|
|
||||||
If you need to rollback:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Drop new tables
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
DROP TABLE IF EXISTS vehicle_options CASCADE;
|
|
||||||
DROP TABLE IF EXISTS transmissions CASCADE;
|
|
||||||
DROP TABLE IF EXISTS engines CASCADE;
|
|
||||||
DROP FUNCTION IF EXISTS get_makes_for_year;
|
|
||||||
DROP FUNCTION IF EXISTS get_models_for_year_make;
|
|
||||||
DROP FUNCTION IF EXISTS get_trims_for_year_make_model;
|
|
||||||
DROP FUNCTION IF EXISTS get_options_for_vehicle;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Restore from backup
|
|
||||||
docker cp data/backups/vehicles_schema_backup_<timestamp>.sql mvp-postgres:/tmp/
|
|
||||||
docker exec -i mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
< /tmp/vehicles_schema_backup_<timestamp>.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handoff to Agent 2
|
|
||||||
|
|
||||||
Once complete, provide this information to Agent 2 (Platform Repository):
|
|
||||||
|
|
||||||
### Database Contract
|
|
||||||
|
|
||||||
**Tables Available:**
|
|
||||||
```sql
|
|
||||||
engines (id, name)
|
|
||||||
transmissions (id, type)
|
|
||||||
vehicle_options (id, year, make, model, trim, engine_id, transmission_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Functions Available:**
|
|
||||||
```sql
|
|
||||||
get_makes_for_year(year INT) → TABLE(make VARCHAR)
|
|
||||||
get_models_for_year_make(year INT, make VARCHAR) → TABLE(model VARCHAR)
|
|
||||||
get_trims_for_year_make_model(year INT, make VARCHAR, model VARCHAR) → TABLE(trim_name VARCHAR)
|
|
||||||
get_options_for_vehicle(year INT, make VARCHAR, model VARCHAR, trim VARCHAR)
|
|
||||||
→ TABLE(engine_name VARCHAR, transmission_type VARCHAR, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Data Quality Notes:**
|
|
||||||
- Makes are in Title Case: "Ford", not "FORD"
|
|
||||||
- 1.1% of records have NULL engine_id (electric vehicles)
|
|
||||||
- Year range: 1980-2026
|
|
||||||
- 53 makes, 1,741 models, 1,122,644 total configurations
|
|
||||||
|
|
||||||
**Performance:**
|
|
||||||
- All queries using indexes perform sub-50ms
|
|
||||||
- Cascade queries optimized with composite indexes
|
|
||||||
|
|
||||||
### Verification Command
|
|
||||||
Agent 2 can verify database is ready:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM vehicle_options;"
|
|
||||||
```
|
|
||||||
Should return: 1122644
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 1 (Database Migration): COMPLETE
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Dropped vehicles.* schema tables (backup created)
|
|
||||||
- Executed 001_create_vehicle_database.sql migration
|
|
||||||
- Imported 30,066 engines
|
|
||||||
- Imported 828 transmissions
|
|
||||||
- Imported 1,122,644 vehicle options
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ All tables created with correct record counts
|
|
||||||
✓ Database functions operational
|
|
||||||
✓ Composite indexes created
|
|
||||||
✓ Query performance sub-50ms
|
|
||||||
✓ Data quality checks passed
|
|
||||||
|
|
||||||
Database is ready for Agent 2 (Platform Repository) to begin implementation.
|
|
||||||
|
|
||||||
Files modified: None (database only)
|
|
||||||
New schema: public.engines, public.transmissions, public.vehicle_options
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
# Frontend API Client Update - Agent 5
|
|
||||||
|
|
||||||
## Task: Update vehicles API client to use string-based parameters and responses
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agent 4 (Vehicles API) must be complete
|
|
||||||
**Estimated Time**: 1 hour
|
|
||||||
**Assigned To**: Agent 5 (Frontend API)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Update the frontend API client to match the new backend API contract. Change from ID-based parameters to string-based parameters, and handle string array responses instead of object arrays.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify Agent 4 Completed
|
|
||||||
```bash
|
|
||||||
# Test backend API returns strings
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
|
|
||||||
-H "Authorization: Bearer TOKEN"
|
|
||||||
# Should return: ["Ford", "Honda", ...] not [{id: 1, name: "Ford"}, ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
```
|
|
||||||
frontend/src/features/vehicles/api/vehicles.api.ts
|
|
||||||
frontend/src/features/vehicles/types/vehicles.types.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Implementation Analysis
|
|
||||||
|
|
||||||
### API Client
|
|
||||||
|
|
||||||
**File**: `frontend/src/features/vehicles/api/vehicles.api.ts`
|
|
||||||
|
|
||||||
**Current Methods** (ID-based parameters, object responses):
|
|
||||||
```typescript
|
|
||||||
getMakes(year: number): Promise<DropdownOption[]>
|
|
||||||
getModels(year: number, makeId: number): Promise<DropdownOption[]>
|
|
||||||
getTrims(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
|
|
||||||
getEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]>
|
|
||||||
getTransmissions(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
|
|
||||||
getYears(): Promise<number[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Definition
|
|
||||||
|
|
||||||
**File**: `frontend/src/features/vehicles/types/vehicles.types.ts`
|
|
||||||
|
|
||||||
**Current DropdownOption**:
|
|
||||||
```typescript
|
|
||||||
export interface DropdownOption {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Update or Remove DropdownOption Type
|
|
||||||
|
|
||||||
**File**: `frontend/src/features/vehicles/types/vehicles.types.ts`
|
|
||||||
|
|
||||||
**Option A: Remove DropdownOption entirely** (Recommended)
|
|
||||||
|
|
||||||
**Find and delete** (lines 56-59):
|
|
||||||
```typescript
|
|
||||||
export interface DropdownOption {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reason**: API now returns plain `string[]`, no need for wrapper type
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Option B: Keep as alias** (If other code depends on it)
|
|
||||||
|
|
||||||
**Replace with**:
|
|
||||||
```typescript
|
|
||||||
// Deprecated: Dropdowns now return string[] directly
|
|
||||||
// Keeping for backward compatibility during migration
|
|
||||||
export type DropdownOption = string;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Recommendation**: Use Option A (delete entirely). Clean break, clear intent.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2: Update API Client Imports
|
|
||||||
|
|
||||||
**File**: `frontend/src/features/vehicles/api/vehicles.api.ts`
|
|
||||||
|
|
||||||
**Current** (line 6):
|
|
||||||
```typescript
|
|
||||||
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DropdownOption, VINDecodeResponse } from '../types/vehicles.types';
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';
|
|
||||||
// DropdownOption removed - using string[] now
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Update getMakes()
|
|
||||||
|
|
||||||
**Current** (lines 41-44):
|
|
||||||
```typescript
|
|
||||||
getMakes: async (year: number): Promise<DropdownOption[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
getMakes: async (year: number): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 41: Return type `Promise<DropdownOption[]>` → `Promise<string[]>`
|
|
||||||
- Query parameter unchanged (already uses year)
|
|
||||||
- Response data is now string array
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Update getModels()
|
|
||||||
|
|
||||||
**Current** (lines 46-49):
|
|
||||||
```typescript
|
|
||||||
getModels: async (year: number, makeId: number): Promise<DropdownOption[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make_id=${makeId}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
getModels: async (year: number, make: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 46: Parameter `makeId: number` → `make: string`
|
|
||||||
- Line 46: Return type `Promise<DropdownOption[]>` → `Promise<string[]>`
|
|
||||||
- Line 47: Query param `make_id=${makeId}` → `make=${encodeURIComponent(make)}`
|
|
||||||
- **Important**: Use `encodeURIComponent()` to handle spaces and special chars in make names
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5: Update getTrims()
|
|
||||||
|
|
||||||
**Current** (lines 61-64):
|
|
||||||
```typescript
|
|
||||||
getTrims: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make_id=${makeId}&model_id=${modelId}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 61: Parameters changed to `make: string, model: string`
|
|
||||||
- Line 61: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 62: Query params changed to `make=...&model=...`
|
|
||||||
- Use `encodeURIComponent()` for both make and model
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6: Update getEngines()
|
|
||||||
|
|
||||||
**Current** (lines 56-59):
|
|
||||||
```typescript
|
|
||||||
getEngines: async (year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make_id=${makeId}&model_id=${modelId}&trim_id=${trimId}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 56: Parameters changed to strings: `make: string, model: string, trim: string`
|
|
||||||
- Line 56: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 57: Query params changed to use strings with proper encoding
|
|
||||||
- Use `encodeURIComponent()` for all string parameters
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7: Update getTransmissions()
|
|
||||||
|
|
||||||
**Current** (lines 51-54):
|
|
||||||
```typescript
|
|
||||||
getTransmissions: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make_id=${makeId}&model_id=${modelId}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes**:
|
|
||||||
- Line 51: Parameters changed to `make: string, model: string`
|
|
||||||
- Line 51: Return type changed to `Promise<string[]>`
|
|
||||||
- Line 52: Query params changed to strings
|
|
||||||
- Use `encodeURIComponent()` for encoding
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Complete Updated File
|
|
||||||
|
|
||||||
After all changes, the vehicles.api.ts file should look like:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @ai-summary API calls for vehicles feature
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { apiClient } from '../../../core/api/client';
|
|
||||||
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';
|
|
||||||
|
|
||||||
// All requests (including dropdowns) use authenticated apiClient
|
|
||||||
|
|
||||||
export const vehiclesApi = {
|
|
||||||
getAll: async (): Promise<Vehicle[]> => {
|
|
||||||
const response = await apiClient.get('/vehicles');
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getById: async (id: string): Promise<Vehicle> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/${id}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
create: async (data: CreateVehicleRequest): Promise<Vehicle> => {
|
|
||||||
const response = await apiClient.post('/vehicles', data);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
update: async (id: string, data: UpdateVehicleRequest): Promise<Vehicle> => {
|
|
||||||
const response = await apiClient.put(`/vehicles/${id}`, data);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.delete(`/vehicles/${id}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Dropdown API methods - now return string[] and accept string parameters
|
|
||||||
getYears: async (): Promise<number[]> => {
|
|
||||||
const response = await apiClient.get('/vehicles/dropdown/years');
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getMakes: async (year: number): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getModels: async (year: number, make: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
|
|
||||||
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
// VIN decode method - using unified platform endpoint
|
|
||||||
decodeVIN: async (vin: string): Promise<VINDecodeResponse> => {
|
|
||||||
const response = await apiClient.get(`/platform/vehicle?vin=${vin}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why encodeURIComponent()?
|
|
||||||
|
|
||||||
**Problem**: Vehicle makes/models can contain spaces and special characters:
|
|
||||||
- "Land Rover"
|
|
||||||
- "Mercedes-Benz"
|
|
||||||
- "Alfa Romeo"
|
|
||||||
|
|
||||||
**Without encoding**: Query string breaks
|
|
||||||
```
|
|
||||||
/vehicles/dropdown/models?year=2024&make=Land Rover
|
|
||||||
^ space breaks URL
|
|
||||||
```
|
|
||||||
|
|
||||||
**With encoding**: Query string is valid
|
|
||||||
```
|
|
||||||
/vehicles/dropdown/models?year=2024&make=Land%20Rover
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rule**: Always encode user-provided strings in URLs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
Test the API client methods in browser console:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In browser console (after logging in)
|
|
||||||
import { vehiclesApi } from './src/features/vehicles/api/vehicles.api';
|
|
||||||
|
|
||||||
// Test getMakes - should return string[]
|
|
||||||
const makes = await vehiclesApi.getMakes(2024);
|
|
||||||
console.log(makes); // Expected: ["Acura", "Audi", "BMW", ...]
|
|
||||||
console.log(typeof makes[0]); // Expected: "string"
|
|
||||||
|
|
||||||
// Test getModels with string parameter
|
|
||||||
const models = await vehiclesApi.getModels(2024, 'Ford');
|
|
||||||
console.log(models); // Expected: ["Bronco", "Edge", "F-150", ...]
|
|
||||||
|
|
||||||
// Test with space in name
|
|
||||||
const landRoverModels = await vehiclesApi.getModels(2024, 'Land Rover');
|
|
||||||
console.log(landRoverModels); // Should work - no URL errors
|
|
||||||
|
|
||||||
// Test getTrims
|
|
||||||
const trims = await vehiclesApi.getTrims(2024, 'Ford', 'F-150');
|
|
||||||
console.log(trims); // Expected: ["XLT", "Lariat", ...]
|
|
||||||
|
|
||||||
// Test getEngines
|
|
||||||
const engines = await vehiclesApi.getEngines(2024, 'Ford', 'F-150', 'XLT');
|
|
||||||
console.log(engines); // Expected: ["V6 2.7L Turbo", "V8 5.0L", ...]
|
|
||||||
|
|
||||||
// Test getTransmissions - should return REAL data now
|
|
||||||
const transmissions = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
|
|
||||||
console.log(transmissions); // Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
Create tests for the API client:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// frontend/src/features/vehicles/api/__tests__/vehicles.api.test.ts
|
|
||||||
|
|
||||||
import { vehiclesApi } from '../vehicles.api';
|
|
||||||
import { apiClient } from '../../../../core/api/client';
|
|
||||||
|
|
||||||
jest.mock('../../../../core/api/client');
|
|
||||||
|
|
||||||
describe('vehiclesApi', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMakes', () => {
|
|
||||||
it('should return string array', async () => {
|
|
||||||
const mockMakes = ['Ford', 'Honda', 'Toyota'];
|
|
||||||
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockMakes });
|
|
||||||
|
|
||||||
const result = await vehiclesApi.getMakes(2024);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockMakes);
|
|
||||||
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/makes?year=2024');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getModels', () => {
|
|
||||||
it('should use make string parameter', async () => {
|
|
||||||
const mockModels = ['F-150', 'Mustang'];
|
|
||||||
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
|
|
||||||
|
|
||||||
const result = await vehiclesApi.getModels(2024, 'Ford');
|
|
||||||
|
|
||||||
expect(result).toEqual(mockModels);
|
|
||||||
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Ford');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encode make parameter with spaces', async () => {
|
|
||||||
const mockModels = ['Range Rover', 'Defender'];
|
|
||||||
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
|
|
||||||
|
|
||||||
await vehiclesApi.getModels(2024, 'Land Rover');
|
|
||||||
|
|
||||||
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Land%20Rover');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encode special characters', async () => {
|
|
||||||
const mockModels = ['C-Class'];
|
|
||||||
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
|
|
||||||
|
|
||||||
await vehiclesApi.getModels(2024, 'Mercedes-Benz');
|
|
||||||
|
|
||||||
expect(apiClient.get).toHaveBeenCalledWith(expect.stringContaining('Mercedes-Benz'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTransmissions', () => {
|
|
||||||
it('should return real transmission data', async () => {
|
|
||||||
const mockTransmissions = ['10-Speed Automatic', '6-Speed Manual'];
|
|
||||||
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockTransmissions });
|
|
||||||
|
|
||||||
const result = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
|
|
||||||
|
|
||||||
expect(result).toEqual(mockTransmissions);
|
|
||||||
// Verify not getting old hardcoded data
|
|
||||||
expect(result).not.toEqual([{id: 1, name: 'Automatic'}, {id: 2, name: 'Manual'}]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm test -- vehicles.api.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion:
|
|
||||||
|
|
||||||
- [ ] DropdownOption type removed (or deprecated)
|
|
||||||
- [ ] Import statement updated (removed DropdownOption)
|
|
||||||
- [ ] getMakes() returns string[]
|
|
||||||
- [ ] getModels() accepts make string, returns string[]
|
|
||||||
- [ ] getTrims() accepts make/model strings, returns string[]
|
|
||||||
- [ ] getEngines() accepts make/model/trim strings, returns string[]
|
|
||||||
- [ ] getTransmissions() accepts make/model strings, returns string[]
|
|
||||||
- [ ] All string parameters use encodeURIComponent()
|
|
||||||
- [ ] Query parameters changed: make_id→make, model_id→model, trim_id→trim
|
|
||||||
- [ ] Manual tests pass (no type errors, correct responses)
|
|
||||||
- [ ] Unit tests pass
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] No console errors in browser
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: TypeScript error "Type 'string[]' is not assignable to 'DropdownOption[]'"
|
|
||||||
**Cause**: Form component (Agent 6) not yet updated, still expects DropdownOption[]
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- This is expected - Agent 6 will update the form component
|
|
||||||
- Verify API client changes are correct
|
|
||||||
- TypeScript errors will resolve once Agent 6 completes
|
|
||||||
|
|
||||||
### Issue: URL encoding breaks query
|
|
||||||
**Cause**: Forgot `encodeURIComponent()` on string parameters
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```typescript
|
|
||||||
// Wrong
|
|
||||||
`/models?make=${make}` // Breaks with "Land Rover"
|
|
||||||
|
|
||||||
// Correct
|
|
||||||
`/models?make=${encodeURIComponent(make)}` // Handles all cases
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Backend returns 400 "make parameter required"
|
|
||||||
**Cause**: Frontend sending wrong parameter name or encoding issue
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Check browser Network tab for actual request
|
|
||||||
- Verify parameter names: `make`, `model`, `trim` (not `make_id`, etc.)
|
|
||||||
- Verify values are properly encoded
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handoff to Agent 6
|
|
||||||
|
|
||||||
Once complete, provide this information:
|
|
||||||
|
|
||||||
### Updated API Client Contract
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
```typescript
|
|
||||||
getYears(): Promise<number[]>
|
|
||||||
getMakes(year: number): Promise<string[]>
|
|
||||||
getModels(year: number, make: string): Promise<string[]>
|
|
||||||
getTrims(year: number, make: string, model: string): Promise<string[]>
|
|
||||||
getEngines(year: number, make: string, model: string, trim: string): Promise<string[]>
|
|
||||||
getTransmissions(year: number, make: string, model: string): Promise<string[]>
|
|
||||||
decodeVIN(vin: string): Promise<VINDecodeResponse>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Changes for Agent 6**:
|
|
||||||
- All dropdown methods return `string[]` (not `DropdownOption[]`)
|
|
||||||
- Cascade queries use selected string values directly (not IDs)
|
|
||||||
- No need to extract `.id` from selected options anymore
|
|
||||||
- No need to find selected option by ID to get name
|
|
||||||
- Simpler state management - store strings directly
|
|
||||||
|
|
||||||
**Example Usage**:
|
|
||||||
```typescript
|
|
||||||
// OLD (ID-based)
|
|
||||||
const [selectedMake, setSelectedMake] = useState<DropdownOption | null>(null);
|
|
||||||
const models = await vehiclesApi.getModels(year, selectedMake.id); // Use ID
|
|
||||||
// Store name: vehicle.make = selectedMake.name
|
|
||||||
|
|
||||||
// NEW (String-based)
|
|
||||||
const [selectedMake, setSelectedMake] = useState<string>('');
|
|
||||||
const models = await vehiclesApi.getModels(year, selectedMake); // Use string directly
|
|
||||||
// Store directly: vehicle.make = selectedMake
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verification Command
|
|
||||||
```bash
|
|
||||||
# Agent 6 can verify API client is ready:
|
|
||||||
cd frontend && npm run build
|
|
||||||
# Should compile (may have type errors in form component - Agent 6 will fix)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 5 (Frontend API Client): COMPLETE
|
|
||||||
|
|
||||||
Files Modified:
|
|
||||||
- frontend/src/features/vehicles/api/vehicles.api.ts
|
|
||||||
- frontend/src/features/vehicles/types/vehicles.types.ts
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Removed DropdownOption interface (using string[] now)
|
|
||||||
- Updated all dropdown methods to return string[]
|
|
||||||
- Changed parameters from IDs to strings (make, model, trim)
|
|
||||||
- Added encodeURIComponent() for URL encoding
|
|
||||||
- Query parameters updated: make_id→make, model_id→model, trim_id→trim
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ TypeScript compiles (with expected form component errors)
|
|
||||||
✓ Manual API tests return correct string arrays
|
|
||||||
✓ Unit tests pass
|
|
||||||
✓ URL encoding handles spaces and special characters
|
|
||||||
✓ Transmissions return real data (not hardcoded)
|
|
||||||
|
|
||||||
Agent 6 (Frontend Forms) can now update VehicleForm component to use new API contract.
|
|
||||||
|
|
||||||
Breaking Changes for Agent 6:
|
|
||||||
- API methods return string[] not DropdownOption[]
|
|
||||||
- No more .id property - use selected strings directly
|
|
||||||
- Simplified cascade logic - no ID lookups needed
|
|
||||||
- Form can store selected values directly as strings
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,818 +0,0 @@
|
|||||||
# Frontend Vehicle Form Update - Agent 6
|
|
||||||
|
|
||||||
## Task: Update VehicleForm component to use string-based dropdowns
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agent 5 (Frontend API Client) must be complete
|
|
||||||
**Estimated Time**: 2-3 hours
|
|
||||||
**Assigned To**: Agent 6 (Frontend Forms)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Simplify the VehicleForm component by removing ID-based logic. Change from tracking `DropdownOption` objects to tracking simple strings, which eliminates complex lookups and simplifies cascade logic.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify Agent 5 Completed
|
|
||||||
```bash
|
|
||||||
# Verify API client compiles
|
|
||||||
cd frontend && npm run build
|
|
||||||
# May have type errors in VehicleForm - that's expected
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
```
|
|
||||||
frontend/src/features/vehicles/components/VehicleForm.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Implementation Problems
|
|
||||||
|
|
||||||
**File**: `frontend/src/features/vehicles/components/VehicleForm.tsx`
|
|
||||||
|
|
||||||
### Problem 1: Complex State Management
|
|
||||||
|
|
||||||
**Current state** (lines 70-80):
|
|
||||||
```typescript
|
|
||||||
const [makes, setMakes] = useState<DropdownOption[]>([]); // {id, name}[]
|
|
||||||
const [models, setModels] = useState<DropdownOption[]>([]);
|
|
||||||
const [engines, setEngines] = useState<DropdownOption[]>([]);
|
|
||||||
const [trims, setTrims] = useState<DropdownOption[]>([]);
|
|
||||||
const [transmissions, setTransmissions] = useState<DropdownOption[]>([]);
|
|
||||||
|
|
||||||
const [selectedMake, setSelectedMake] = useState<DropdownOption | undefined>();
|
|
||||||
const [selectedModel, setSelectedModel] = useState<DropdownOption | undefined>();
|
|
||||||
const [selectedTrim, setSelectedTrim] = useState<DropdownOption | undefined>();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem**: Tracking both arrays AND selected objects, requires lookups by ID
|
|
||||||
|
|
||||||
### Problem 2: Cascade Logic Complexity
|
|
||||||
|
|
||||||
**Current pattern**:
|
|
||||||
```typescript
|
|
||||||
// When user selects make from dropdown
|
|
||||||
// 1. Find selected option object by ID from makes array
|
|
||||||
// 2. Store entire {id, name} object
|
|
||||||
// 3. Extract .id to call API for next level
|
|
||||||
// 4. Extract .name to store in form
|
|
||||||
|
|
||||||
// Example:
|
|
||||||
const selectedMakeObj = makes.find(m => m.id === selectedMakeId);
|
|
||||||
setSelectedMake(selectedMakeObj);
|
|
||||||
const models = await vehiclesApi.getModels(year, selectedMakeObj.id); // Use ID for API
|
|
||||||
setValue('make', selectedMakeObj.name); // Store name in form
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem**: Unnecessary indirection - we just need the string!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution: String-Based State
|
|
||||||
|
|
||||||
### New State (Simplified)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Dropdown options are just string arrays
|
|
||||||
const [makes, setMakes] = useState<string[]>([]);
|
|
||||||
const [models, setModels] = useState<string[]>([]);
|
|
||||||
const [engines, setEngines] = useState<string[]>([]);
|
|
||||||
const [trims, setTrims] = useState<string[]>([]);
|
|
||||||
const [transmissions, setTransmissions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Selected values are just strings
|
|
||||||
const [selectedMake, setSelectedMake] = useState<string>('');
|
|
||||||
const [selectedModel, setSelectedModel] = useState<string>('');
|
|
||||||
const [selectedTrim, setSelectedTrim] = useState<string>('');
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- No more DropdownOption type
|
|
||||||
- No more ID lookups
|
|
||||||
- Direct string values
|
|
||||||
- Simpler cascade logic
|
|
||||||
|
|
||||||
### New Cascade Pattern
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// When user selects make from dropdown
|
|
||||||
// 1. Store string directly
|
|
||||||
// 2. Use string directly for API call
|
|
||||||
// 3. Use string directly in form
|
|
||||||
|
|
||||||
// Example:
|
|
||||||
setSelectedMake(makeValue); // Just the string
|
|
||||||
const models = await vehiclesApi.getModels(year, makeValue); // Use string for API
|
|
||||||
setValue('make', makeValue); // Store string in form
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### Step 1: Update Imports
|
|
||||||
|
|
||||||
**Current** (line 10):
|
|
||||||
```typescript
|
|
||||||
import { CreateVehicleRequest, DropdownOption } from '../types/vehicles.types';
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
import { CreateVehicleRequest } from '../types/vehicles.types';
|
|
||||||
// DropdownOption removed - using string arrays now
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2: Update State Declarations
|
|
||||||
|
|
||||||
**Current** (lines 70-80):
|
|
||||||
```typescript
|
|
||||||
const [makes, setMakes] = useState<DropdownOption[]>([]);
|
|
||||||
const [models, setModels] = useState<DropdownOption[]>([]);
|
|
||||||
const [engines, setEngines] = useState<DropdownOption[]>([]);
|
|
||||||
const [trims, setTrims] = useState<DropdownOption[]>([]);
|
|
||||||
const [transmissions, setTransmissions] = useState<DropdownOption[]>([]);
|
|
||||||
|
|
||||||
const [selectedMake, setSelectedMake] = useState<DropdownOption | undefined>();
|
|
||||||
const [selectedModel, setSelectedModel] = useState<DropdownOption | undefined>();
|
|
||||||
const [selectedTrim, setSelectedTrim] = useState<DropdownOption | undefined>();
|
|
||||||
```
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
const [makes, setMakes] = useState<string[]>([]);
|
|
||||||
const [models, setModels] = useState<string[]>([]);
|
|
||||||
const [engines, setEngines] = useState<string[]>([]);
|
|
||||||
const [trims, setTrims] = useState<string[]>([]);
|
|
||||||
const [transmissions, setTransmissions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [selectedMake, setSelectedMake] = useState<string>('');
|
|
||||||
const [selectedModel, setSelectedModel] = useState<string>('');
|
|
||||||
const [selectedTrim, setSelectedTrim] = useState<string>('');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Update Cascade Logic
|
|
||||||
|
|
||||||
Find all useEffect hooks that load dropdowns and simplify them.
|
|
||||||
|
|
||||||
#### Example: Load Makes (when year changes)
|
|
||||||
|
|
||||||
**Current pattern**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!watchedYear) return;
|
|
||||||
|
|
||||||
const loadMakes = async () => {
|
|
||||||
setLoadingDropdowns(true);
|
|
||||||
try {
|
|
||||||
const makesData = await vehiclesApi.getMakes(watchedYear);
|
|
||||||
setMakes(makesData); // DropdownOption[]
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load makes:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingDropdowns(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadMakes();
|
|
||||||
// Reset dependent selections
|
|
||||||
setSelectedMake(undefined);
|
|
||||||
setModels([]);
|
|
||||||
setSelectedModel(undefined);
|
|
||||||
// ... etc
|
|
||||||
}, [watchedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
**New (simplified)**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedYear) return;
|
|
||||||
|
|
||||||
const loadMakes = async () => {
|
|
||||||
setLoadingDropdowns(true);
|
|
||||||
try {
|
|
||||||
const makesData = await vehiclesApi.getMakes(selectedYear);
|
|
||||||
setMakes(makesData); // string[]
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load makes:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingDropdowns(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadMakes();
|
|
||||||
// Reset dependent selections
|
|
||||||
setSelectedMake('');
|
|
||||||
setModels([]);
|
|
||||||
setSelectedModel('');
|
|
||||||
setSelectedTrim('');
|
|
||||||
setTrims([]);
|
|
||||||
setEngines([]);
|
|
||||||
setTransmissions([]);
|
|
||||||
}, [selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example: Load Models (when make changes)
|
|
||||||
|
|
||||||
**Current pattern**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedMake || !selectedYear) return;
|
|
||||||
|
|
||||||
const loadModels = async () => {
|
|
||||||
try {
|
|
||||||
// Use selectedMake.id for API call
|
|
||||||
const modelsData = await vehiclesApi.getModels(selectedYear, selectedMake.id);
|
|
||||||
setModels(modelsData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load models:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadModels();
|
|
||||||
}, [selectedMake, selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
**New (simplified)**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedMake || !selectedYear) return;
|
|
||||||
|
|
||||||
const loadModels = async () => {
|
|
||||||
try {
|
|
||||||
// Use selectedMake string directly
|
|
||||||
const modelsData = await vehiclesApi.getModels(selectedYear, selectedMake);
|
|
||||||
setModels(modelsData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load models:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadModels();
|
|
||||||
// Reset dependent selections
|
|
||||||
setSelectedModel('');
|
|
||||||
setSelectedTrim('');
|
|
||||||
setTrims([]);
|
|
||||||
setEngines([]);
|
|
||||||
setTransmissions([]);
|
|
||||||
}, [selectedMake, selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example: Load Trims (when model changes)
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedModel || !selectedMake || !selectedYear) return;
|
|
||||||
|
|
||||||
const loadTrims = async () => {
|
|
||||||
try {
|
|
||||||
const trimsData = await vehiclesApi.getTrims(selectedYear, selectedMake, selectedModel);
|
|
||||||
setTrims(trimsData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load trims:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadTrims();
|
|
||||||
setSelectedTrim('');
|
|
||||||
setEngines([]);
|
|
||||||
setTransmissions([]);
|
|
||||||
}, [selectedModel, selectedMake, selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example: Load Engines (when trim changes)
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedTrim || !selectedModel || !selectedMake || !selectedYear) return;
|
|
||||||
|
|
||||||
const loadEngines = async () => {
|
|
||||||
try {
|
|
||||||
const enginesData = await vehiclesApi.getEngines(
|
|
||||||
selectedYear,
|
|
||||||
selectedMake,
|
|
||||||
selectedModel,
|
|
||||||
selectedTrim
|
|
||||||
);
|
|
||||||
setEngines(enginesData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load engines:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadEngines();
|
|
||||||
}, [selectedTrim, selectedModel, selectedMake, selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example: Load Transmissions (when model changes)
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedModel || !selectedMake || !selectedYear) return;
|
|
||||||
|
|
||||||
const loadTransmissions = async () => {
|
|
||||||
try {
|
|
||||||
const transmissionsData = await vehiclesApi.getTransmissions(
|
|
||||||
selectedYear,
|
|
||||||
selectedMake,
|
|
||||||
selectedModel
|
|
||||||
);
|
|
||||||
setTransmissions(transmissionsData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load transmissions:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadTransmissions();
|
|
||||||
}, [selectedModel, selectedMake, selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4: Update Dropdown onChange Handlers
|
|
||||||
|
|
||||||
#### Year Selection
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('year', { valueAsNumber: true })}
|
|
||||||
value={selectedYear || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const year = parseInt(e.target.value);
|
|
||||||
setSelectedYear(year);
|
|
||||||
setValue('year', year);
|
|
||||||
}}
|
|
||||||
disabled={loadingDropdowns}
|
|
||||||
>
|
|
||||||
<option value="">Select Year</option>
|
|
||||||
{years.map((year) => (
|
|
||||||
<option key={year} value={year}>
|
|
||||||
{year}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Make Selection
|
|
||||||
|
|
||||||
**Old pattern**:
|
|
||||||
```typescript
|
|
||||||
onChange={(e) => {
|
|
||||||
const makeId = parseInt(e.target.value);
|
|
||||||
const makeObj = makes.find(m => m.id === makeId);
|
|
||||||
setSelectedMake(makeObj);
|
|
||||||
setValue('make', makeObj?.name || '');
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New (simplified)**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('make')}
|
|
||||||
value={selectedMake}
|
|
||||||
onChange={(e) => {
|
|
||||||
const make = e.target.value;
|
|
||||||
setSelectedMake(make);
|
|
||||||
setValue('make', make);
|
|
||||||
}}
|
|
||||||
disabled={!selectedYear || makes.length === 0 || loadingDropdowns}
|
|
||||||
>
|
|
||||||
<option value="">Select Make</option>
|
|
||||||
{makes.map((make) => (
|
|
||||||
<option key={make} value={make}>
|
|
||||||
{make}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Model Selection
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('model')}
|
|
||||||
value={selectedModel}
|
|
||||||
onChange={(e) => {
|
|
||||||
const model = e.target.value;
|
|
||||||
setSelectedModel(model);
|
|
||||||
setValue('model', model);
|
|
||||||
}}
|
|
||||||
disabled={!selectedMake || models.length === 0 || loadingDropdowns}
|
|
||||||
>
|
|
||||||
<option value="">Select Model</option>
|
|
||||||
{models.map((model) => (
|
|
||||||
<option key={model} value={model}>
|
|
||||||
{model}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Trim Selection
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('trimLevel')}
|
|
||||||
value={selectedTrim}
|
|
||||||
onChange={(e) => {
|
|
||||||
const trim = e.target.value;
|
|
||||||
setSelectedTrim(trim);
|
|
||||||
setValue('trimLevel', trim);
|
|
||||||
}}
|
|
||||||
disabled={!selectedModel || trims.length === 0 || loadingDropdowns}
|
|
||||||
>
|
|
||||||
<option value="">Select Trim</option>
|
|
||||||
{trims.map((trim) => (
|
|
||||||
<option key={trim} value={trim}>
|
|
||||||
{trim}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Engine Selection
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('engine')}
|
|
||||||
disabled={!selectedTrim || engines.length === 0}
|
|
||||||
>
|
|
||||||
<option value="">Select Engine</option>
|
|
||||||
{engines.map((engine) => (
|
|
||||||
<option key={engine} value={engine}>
|
|
||||||
{engine}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: Engine field shows 'N/A (Electric)' for electric vehicles automatically
|
|
||||||
|
|
||||||
#### Transmission Selection
|
|
||||||
|
|
||||||
**New**:
|
|
||||||
```typescript
|
|
||||||
<select
|
|
||||||
{...register('transmission')}
|
|
||||||
disabled={!selectedModel || transmissions.length === 0}
|
|
||||||
>
|
|
||||||
<option value="">Select Transmission</option>
|
|
||||||
{transmissions.map((transmission) => (
|
|
||||||
<option key={transmission} value={transmission}>
|
|
||||||
{transmission}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5: Update Edit Mode Initialization
|
|
||||||
|
|
||||||
When editing an existing vehicle, the form needs to:
|
|
||||||
1. Load all cascading dropdowns in order
|
|
||||||
2. Pre-select the values from initialData
|
|
||||||
|
|
||||||
**New initialization pattern**:
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initialData || hasInitialized.current || isInitializing.current) return;
|
|
||||||
|
|
||||||
const initializeEditMode = async () => {
|
|
||||||
isInitializing.current = true;
|
|
||||||
setLoadingDropdowns(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Load years
|
|
||||||
const yearsData = await vehiclesApi.getYears();
|
|
||||||
setYears(yearsData);
|
|
||||||
|
|
||||||
if (!initialData.year) {
|
|
||||||
setDropdownsReady(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedYear(initialData.year);
|
|
||||||
|
|
||||||
// Step 2: Load makes for year
|
|
||||||
const makesData = await vehiclesApi.getMakes(initialData.year);
|
|
||||||
setMakes(makesData);
|
|
||||||
|
|
||||||
if (!initialData.make) {
|
|
||||||
setDropdownsReady(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedMake(initialData.make);
|
|
||||||
|
|
||||||
// Step 3: Load models for year + make
|
|
||||||
const modelsData = await vehiclesApi.getModels(initialData.year, initialData.make);
|
|
||||||
setModels(modelsData);
|
|
||||||
|
|
||||||
if (!initialData.model) {
|
|
||||||
setDropdownsReady(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedModel(initialData.model);
|
|
||||||
|
|
||||||
// Step 4: Load trims for year + make + model
|
|
||||||
if (initialData.trimLevel) {
|
|
||||||
const trimsData = await vehiclesApi.getTrims(
|
|
||||||
initialData.year,
|
|
||||||
initialData.make,
|
|
||||||
initialData.model
|
|
||||||
);
|
|
||||||
setTrims(trimsData);
|
|
||||||
setSelectedTrim(initialData.trimLevel);
|
|
||||||
|
|
||||||
// Step 5: Load engines for full selection
|
|
||||||
const enginesData = await vehiclesApi.getEngines(
|
|
||||||
initialData.year,
|
|
||||||
initialData.make,
|
|
||||||
initialData.model,
|
|
||||||
initialData.trimLevel
|
|
||||||
);
|
|
||||||
setEngines(enginesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Load transmissions for year + make + model
|
|
||||||
const transmissionsData = await vehiclesApi.getTransmissions(
|
|
||||||
initialData.year,
|
|
||||||
initialData.make,
|
|
||||||
initialData.model
|
|
||||||
);
|
|
||||||
setTransmissions(transmissionsData);
|
|
||||||
|
|
||||||
setDropdownsReady(true);
|
|
||||||
hasInitialized.current = true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize edit mode:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingDropdowns(false);
|
|
||||||
isInitializing.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeEditMode();
|
|
||||||
}, [initialData]);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 6: Handle Empty States and Loading
|
|
||||||
|
|
||||||
Display appropriate messages for empty dropdowns:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example for makes dropdown
|
|
||||||
<select {...register('make')} disabled={!selectedYear || loadingDropdowns}>
|
|
||||||
<option value="">
|
|
||||||
{loadingDropdowns
|
|
||||||
? 'Loading...'
|
|
||||||
: !selectedYear
|
|
||||||
? 'Select year first'
|
|
||||||
: makes.length === 0
|
|
||||||
? 'No makes available'
|
|
||||||
: 'Select Make'}
|
|
||||||
</option>
|
|
||||||
{makes.map((make) => (
|
|
||||||
<option key={make} value={make}>
|
|
||||||
{make}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Simplifications Summary
|
|
||||||
|
|
||||||
### Before (ID-Based)
|
|
||||||
```typescript
|
|
||||||
// State
|
|
||||||
const [makes, setMakes] = useState<DropdownOption[]>([]);
|
|
||||||
const [selectedMake, setSelectedMake] = useState<DropdownOption | undefined>();
|
|
||||||
|
|
||||||
// API Call
|
|
||||||
const makesData = await vehiclesApi.getMakes(year); // Returns {id, name}[]
|
|
||||||
setMakes(makesData);
|
|
||||||
|
|
||||||
// User Selection
|
|
||||||
onChange={(e) => {
|
|
||||||
const makeId = parseInt(e.target.value);
|
|
||||||
const makeObj = makes.find(m => m.id === makeId); // Lookup by ID
|
|
||||||
setSelectedMake(makeObj);
|
|
||||||
setValue('make', makeObj?.name || ''); // Extract name
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Next API Call
|
|
||||||
const models = await vehiclesApi.getModels(year, selectedMake.id); // Extract ID
|
|
||||||
|
|
||||||
// Render
|
|
||||||
{makes.map((make) => (
|
|
||||||
<option key={make.id} value={make.id}>{make.name}</option>
|
|
||||||
))}
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (String-Based)
|
|
||||||
```typescript
|
|
||||||
// State
|
|
||||||
const [makes, setMakes] = useState<string[]>([]);
|
|
||||||
const [selectedMake, setSelectedMake] = useState<string>('');
|
|
||||||
|
|
||||||
// API Call
|
|
||||||
const makesData = await vehiclesApi.getMakes(year); // Returns string[]
|
|
||||||
setMakes(makesData);
|
|
||||||
|
|
||||||
// User Selection
|
|
||||||
onChange={(e) => {
|
|
||||||
const make = e.target.value; // Just the string
|
|
||||||
setSelectedMake(make);
|
|
||||||
setValue('make', make);
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Next API Call
|
|
||||||
const models = await vehiclesApi.getModels(year, selectedMake); // Use string directly
|
|
||||||
|
|
||||||
// Render
|
|
||||||
{makes.map((make) => (
|
|
||||||
<option key={make} value={make}>{make}</option>
|
|
||||||
))}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lines of code removed**: ~30-40%
|
|
||||||
**Complexity reduced**: Significant (no ID lookups, no object tracking)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
Test both create and edit modes:
|
|
||||||
|
|
||||||
**Create Mode**:
|
|
||||||
1. Open create vehicle form
|
|
||||||
2. Select Year → Verify Makes load
|
|
||||||
3. Select Make → Verify Models load
|
|
||||||
4. Select Model → Verify Trims and Transmissions load
|
|
||||||
5. Select Trim → Verify Engines load
|
|
||||||
6. Verify electric vehicles show "N/A (Electric)" for engines
|
|
||||||
7. Submit form → Verify correct string values saved
|
|
||||||
|
|
||||||
**Edit Mode**:
|
|
||||||
1. Open edit form for existing vehicle
|
|
||||||
2. Verify all dropdowns pre-populate correctly
|
|
||||||
3. Verify selected values display correctly
|
|
||||||
4. Change year → Verify cascade resets correctly
|
|
||||||
5. Change make → Verify models reload
|
|
||||||
6. Submit → Verify updates save correctly
|
|
||||||
|
|
||||||
**VIN Decode**:
|
|
||||||
1. Enter 17-character VIN
|
|
||||||
2. Click decode
|
|
||||||
3. Verify dropdowns auto-populate
|
|
||||||
4. Verify cascade works after decode
|
|
||||||
|
|
||||||
### Mobile Testing (REQUIRED per CLAUDE.md)
|
|
||||||
|
|
||||||
Test on mobile viewport:
|
|
||||||
1. Dropdowns render correctly
|
|
||||||
2. Touch interactions work smoothly
|
|
||||||
3. Loading states visible
|
|
||||||
4. No layout overflow
|
|
||||||
5. Form submission works
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
Before signaling completion:
|
|
||||||
|
|
||||||
- [ ] DropdownOption import removed
|
|
||||||
- [ ] All state changed to string arrays
|
|
||||||
- [ ] Selected value state changed to strings
|
|
||||||
- [ ] All useEffect hooks updated for cascade logic
|
|
||||||
- [ ] All onChange handlers simplified
|
|
||||||
- [ ] Edit mode initialization updated
|
|
||||||
- [ ] Empty state messages added
|
|
||||||
- [ ] VIN decode still works
|
|
||||||
- [ ] Create mode tested successfully
|
|
||||||
- [ ] Edit mode tested successfully
|
|
||||||
- [ ] Mobile responsiveness verified
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] No console errors in browser
|
|
||||||
- [ ] Electric vehicles show correctly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: Dropdowns not cascading
|
|
||||||
**Cause**: Dependencies in useEffect not correct
|
|
||||||
|
|
||||||
**Solution**: Ensure useEffect dependencies include all required selections:
|
|
||||||
```typescript
|
|
||||||
// Models needs year AND make
|
|
||||||
useEffect(() => {
|
|
||||||
// ...
|
|
||||||
}, [selectedYear, selectedMake]);
|
|
||||||
|
|
||||||
// Trims needs year, make, AND model
|
|
||||||
useEffect(() => {
|
|
||||||
// ...
|
|
||||||
}, [selectedYear, selectedMake, selectedModel]);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Edit mode not pre-populating
|
|
||||||
**Cause**: Initialization logic not awaiting each step
|
|
||||||
|
|
||||||
**Solution**: Ensure async/await cascade:
|
|
||||||
```typescript
|
|
||||||
// Load years first, THEN makes, THEN models...
|
|
||||||
const yearsData = await vehiclesApi.getYears();
|
|
||||||
setYears(yearsData);
|
|
||||||
const makesData = await vehiclesApi.getMakes(initialData.year); // After years
|
|
||||||
setMakes(makesData);
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Dropdowns reset unexpectedly
|
|
||||||
**Cause**: useEffect cascade not resetting dependent state
|
|
||||||
|
|
||||||
**Solution**: Reset all downstream state when upstream changes:
|
|
||||||
```typescript
|
|
||||||
// When year changes, reset EVERYTHING downstream
|
|
||||||
useEffect(() => {
|
|
||||||
// ...
|
|
||||||
setSelectedMake('');
|
|
||||||
setModels([]);
|
|
||||||
setSelectedModel('');
|
|
||||||
setTrims([]);
|
|
||||||
setSelectedTrim('');
|
|
||||||
setEngines([]);
|
|
||||||
setTransmissions([]);
|
|
||||||
}, [selectedYear]);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 6 (Frontend Forms): COMPLETE
|
|
||||||
|
|
||||||
Files Modified:
|
|
||||||
- frontend/src/features/vehicles/components/VehicleForm.tsx
|
|
||||||
|
|
||||||
Changes Made:
|
|
||||||
- Removed DropdownOption type usage (using string arrays)
|
|
||||||
- Simplified state management (strings instead of objects)
|
|
||||||
- Updated all cascade useEffect hooks
|
|
||||||
- Simplified onChange handlers (no ID lookups)
|
|
||||||
- Updated edit mode initialization for string-based flow
|
|
||||||
- Added empty state messages
|
|
||||||
- Maintained VIN decode functionality
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
✓ TypeScript compiles successfully
|
|
||||||
✓ Create mode works end-to-end
|
|
||||||
✓ Edit mode pre-populates correctly
|
|
||||||
✓ Cascade dropdowns work properly
|
|
||||||
✓ VIN decode auto-populates correctly
|
|
||||||
✓ Electric vehicles show "N/A (Electric)"
|
|
||||||
✓ Mobile responsiveness verified
|
|
||||||
✓ No console errors
|
|
||||||
|
|
||||||
Agent 7 (Testing) can now perform comprehensive testing of the entire migration.
|
|
||||||
|
|
||||||
Key Improvements:
|
|
||||||
- ~30-40% less code
|
|
||||||
- Eliminated complex ID lookup logic
|
|
||||||
- Direct string value management
|
|
||||||
- Simpler cascade dependencies
|
|
||||||
- Better maintainability
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Platform Vehicle Data Loader Refresh
|
|
||||||
|
|
||||||
## Context
|
|
||||||
- Reintroduced the normalized `vehicles` schema (make/model/model_year/trim/engine + bridges) with a new migration under `backend/src/features/platform/migrations/001_create_vehicle_lookup_schema.sql`.
|
|
||||||
- Added `scripts/load_vehicle_data.py`, a stand-alone Python loader that hydrates the schema from `data/make-models/*.json`.
|
|
||||||
- Loader truncates lookup tables, seeds default transmissions (Automatic, Manual), and inserts deduplicated year → make → model → trim → engine combinations.
|
|
||||||
|
|
||||||
## Follow-up Tasks (Backend API Team)
|
|
||||||
1. **Wire dropdown API to refreshed data**
|
|
||||||
- Run `make migrate` (or `npm run migrate:all` inside backend container) to ensure the new schema exists.
|
|
||||||
- Execute the loader (see command below) so Postgres has the latest lookup entries.
|
|
||||||
- Verify both the platform dropdown queries and the admin catalog APIs surface the same data set (admin APIs now read directly from `vehicles.*` tables).
|
|
||||||
2. **Add Makefile wrapper**
|
|
||||||
- Create a `make load-vehicle-data` task that shells into the backend container, installs `psycopg` if needed, and invokes `python3 scripts/load_vehicle_data.py` with the correct DB credentials and data directory.
|
|
||||||
|
|
||||||
### Loader Command Reference
|
|
||||||
```
|
|
||||||
PGPASSWORD=$(cat secrets/app/postgres-password.txt) \
|
|
||||||
python3 scripts/load_vehicle_data.py \
|
|
||||||
--db-host 127.0.0.1 \
|
|
||||||
--db-port 5432 \
|
|
||||||
--db-user postgres \
|
|
||||||
--db-name motovaultpro \
|
|
||||||
--data-dir data/make-models
|
|
||||||
```
|
|
||||||
|
|
||||||
> Run the command from the repository root (outside of containers) while `mvp-postgres` is up. Adjust host/port if executing inside a container.
|
|
||||||
|
|
||||||
## 2025-11-07 Update
|
|
||||||
- Admin catalog CRUD endpoints have been refactored to read/write the normalized `vehicles.*` tables instead of the legacy `vehicle_dropdown_cache`.
|
|
||||||
- Platform cache invalidation now happens immediately after each catalog mutation so both the admin UI and the user dropdown APIs stay in sync.
|
|
||||||
@@ -1,702 +0,0 @@
|
|||||||
# Testing & Validation Guide - Agent 7
|
|
||||||
|
|
||||||
## Task: Comprehensive testing of vehicle dropdown migration
|
|
||||||
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
**Dependencies**: Agents 1-6 must be complete
|
|
||||||
**Estimated Time**: 2-3 hours
|
|
||||||
**Assigned To**: Agent 7 (Testing & Quality Assurance)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Perform comprehensive end-to-end testing of the entire vehicle dropdown migration. Validate that the new database, API, and frontend work correctly together. Ensure no regressions and verify all quality requirements per CLAUDE.md.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Verify All Agents Completed
|
|
||||||
|
|
||||||
**Agent 1 (Database)**:
|
|
||||||
```bash
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(*) FROM vehicle_options;"
|
|
||||||
# Should return: 1122644
|
|
||||||
```
|
|
||||||
|
|
||||||
**Agent 2-3 (Platform)**:
|
|
||||||
```bash
|
|
||||||
cd backend && npm run build
|
|
||||||
# Should compile with no errors
|
|
||||||
```
|
|
||||||
|
|
||||||
**Agent 4 (Vehicles API)**:
|
|
||||||
```bash
|
|
||||||
cd backend && npm test
|
|
||||||
# Backend tests should pass
|
|
||||||
```
|
|
||||||
|
|
||||||
**Agent 5-6 (Frontend)**:
|
|
||||||
```bash
|
|
||||||
cd frontend && npm run build
|
|
||||||
# Should compile with no errors
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Plan Overview
|
|
||||||
|
|
||||||
### Test Categories
|
|
||||||
|
|
||||||
1. **Database Tests** - Verify data quality and query performance
|
|
||||||
2. **Backend API Tests** - Verify endpoints return correct data
|
|
||||||
3. **Frontend Integration Tests** - Verify UI works end-to-end
|
|
||||||
4. **Mobile Tests** - Verify mobile responsiveness (REQUIRED)
|
|
||||||
5. **Regression Tests** - Verify no existing features broken
|
|
||||||
6. **Performance Tests** - Verify query times meet requirements
|
|
||||||
7. **Edge Case Tests** - Verify error handling and special cases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Database Tests
|
|
||||||
|
|
||||||
### Test 1.1: Data Integrity
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify record counts
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*) FROM engines) as engines,
|
|
||||||
(SELECT COUNT(*) FROM transmissions) as transmissions,
|
|
||||||
(SELECT COUNT(*) FROM vehicle_options) as vehicle_options;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected**:
|
|
||||||
- engines: 30,066
|
|
||||||
- transmissions: 828
|
|
||||||
- vehicle_options: 1,122,644
|
|
||||||
|
|
||||||
### Test 1.2: Data Quality
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify year range
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT MIN(year), MAX(year), COUNT(DISTINCT year) FROM vehicle_options;"
|
|
||||||
# Expected: 1980, 2026, 47
|
|
||||||
|
|
||||||
# Verify make count
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT COUNT(DISTINCT make) FROM vehicle_options;"
|
|
||||||
# Expected: 53
|
|
||||||
|
|
||||||
# Verify make names are Title Case
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT DISTINCT make FROM vehicle_options ORDER BY make LIMIT 10;"
|
|
||||||
# Expected: "Acura", "Alfa Romeo", "Aston Martin", etc. (NOT "ACURA" or "acura")
|
|
||||||
|
|
||||||
# Verify NULL handling
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(*) FILTER (WHERE engine_id IS NULL) as null_engines,
|
|
||||||
ROUND(100.0 * COUNT(*) FILTER (WHERE engine_id IS NULL) / COUNT(*), 2) as percentage
|
|
||||||
FROM vehicle_options;
|
|
||||||
EOF
|
|
||||||
# Expected: ~1.1% NULL engine_id
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 1.3: Database Functions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test get_makes_for_year
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_makes_for_year(2024) LIMIT 5;"
|
|
||||||
# Expected: Returns make names as strings
|
|
||||||
|
|
||||||
# Test get_models_for_year_make
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_models_for_year_make(2024, 'Ford') LIMIT 5;"
|
|
||||||
# Expected: Returns model names
|
|
||||||
|
|
||||||
# Test get_trims_for_year_make_model
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_trims_for_year_make_model(2024, 'Ford', 'F-150') LIMIT 5;"
|
|
||||||
# Expected: Returns trim names
|
|
||||||
|
|
||||||
# Test get_options_for_vehicle
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro \
|
|
||||||
-c "SELECT * FROM get_options_for_vehicle(2024, 'Ford', 'F-150', 'XLT') LIMIT 5;"
|
|
||||||
# Expected: Returns engine/transmission combinations
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 1.4: Query Performance
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test cascade query performance
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
EXPLAIN ANALYZE
|
|
||||||
SELECT DISTINCT make FROM vehicle_options WHERE year = 2024;
|
|
||||||
EOF
|
|
||||||
# Expected: Execution time < 50ms, should use idx_vehicle_year_make
|
|
||||||
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
EXPLAIN ANALYZE
|
|
||||||
SELECT DISTINCT model FROM vehicle_options WHERE year = 2024 AND make = 'Ford';
|
|
||||||
EOF
|
|
||||||
# Expected: Execution time < 50ms, should use idx_vehicle_year_make_model
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All record counts correct
|
|
||||||
- ✅ Year range 1980-2026
|
|
||||||
- ✅ Make names in Title Case
|
|
||||||
- ✅ ~1.1% NULL engine_id
|
|
||||||
- ✅ All database functions return data
|
|
||||||
- ✅ Query performance < 50ms
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Backend API Tests
|
|
||||||
|
|
||||||
### Test 2.1: API Endpoint Responses
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get auth token first
|
|
||||||
TOKEN="your_auth_token_here"
|
|
||||||
|
|
||||||
# Test getYears
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/years" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: [2026, 2025, 2024, ..., 1980]
|
|
||||||
|
|
||||||
# Test getMakes
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["Acura", "Audi", "BMW", ...] (string array, not objects)
|
|
||||||
|
|
||||||
# Test getModels (NOTE: make parameter, not make_id)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Ford" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["Bronco", "Edge", "Escape", ...] (string array)
|
|
||||||
|
|
||||||
# Test getTrims
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["King Ranch", "Lariat", "XLT", ...] (string array)
|
|
||||||
|
|
||||||
# Test getEngines
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["V6 2.7L Turbo", "V8 5.0L", ...] (string array)
|
|
||||||
|
|
||||||
# Test getTransmissions (should return REAL data now, not hardcoded)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["10-Speed Automatic", ...] NOT [{"id": 1, "name": "Automatic"}]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2.2: String Parameters with Special Characters
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test make with space
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Land%20Rover" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: Should work, return Land Rover models
|
|
||||||
|
|
||||||
# Test make with hyphen
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Mercedes-Benz" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: Should work, return Mercedes-Benz models
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2.3: Electric Vehicles (NULL Engine Handling)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test Tesla (should show "N/A (Electric)" for engine)
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Tesla&model=Model%203&trim=Long%20Range" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: ["N/A (Electric)"] or similar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2.4: Error Handling
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test missing parameters
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: 400 Bad Request - "make parameter required"
|
|
||||||
|
|
||||||
# Test invalid year
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=1900" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: 400 Bad Request - "Valid year parameter is required"
|
|
||||||
|
|
||||||
# Test empty string parameter
|
|
||||||
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=" \
|
|
||||||
-H "Authorization: Bearer $TOKEN"
|
|
||||||
# Expected: 400 Bad Request
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All endpoints return string[] (not objects)
|
|
||||||
- ✅ Query parameters use strings (make, model, trim)
|
|
||||||
- ✅ Special characters handled correctly
|
|
||||||
- ✅ Electric vehicles show "N/A (Electric)"
|
|
||||||
- ✅ Transmissions return real data (not hardcoded)
|
|
||||||
- ✅ Error handling works correctly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Frontend Integration Tests
|
|
||||||
|
|
||||||
### Test 3.1: Create Vehicle Form - Full Flow
|
|
||||||
|
|
||||||
**Manual Test Steps**:
|
|
||||||
1. Navigate to Create Vehicle page
|
|
||||||
2. Verify "Select Year" dropdown appears
|
|
||||||
3. Select year (e.g., 2024)
|
|
||||||
4. Verify "Makes" dropdown loads with data
|
|
||||||
5. Verify Makes are strings (not objects) in dev tools
|
|
||||||
6. Select make (e.g., "Ford")
|
|
||||||
7. Verify "Models" dropdown loads
|
|
||||||
8. Verify "Transmissions" dropdown loads simultaneously
|
|
||||||
9. Select model (e.g., "F-150")
|
|
||||||
10. Verify "Trims" dropdown loads
|
|
||||||
11. Verify transmissions update if needed
|
|
||||||
12. Select trim (e.g., "XLT")
|
|
||||||
13. Verify "Engines" dropdown loads
|
|
||||||
14. Verify real transmission data (not "Automatic"/"Manual")
|
|
||||||
15. Select engine
|
|
||||||
16. Fill in other fields (nickname, odometer, etc.)
|
|
||||||
17. Submit form
|
|
||||||
18. Verify vehicle created successfully
|
|
||||||
19. Verify saved vehicle has correct string values
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All dropdowns cascade correctly
|
|
||||||
- ✅ No console errors
|
|
||||||
- ✅ Loading states show correctly
|
|
||||||
- ✅ Disabled states work correctly
|
|
||||||
- ✅ Form submits successfully
|
|
||||||
- ✅ Data saves with correct values
|
|
||||||
|
|
||||||
### Test 3.2: Edit Vehicle Form - Initialization
|
|
||||||
|
|
||||||
**Manual Test Steps**:
|
|
||||||
1. Navigate to an existing vehicle's edit page
|
|
||||||
2. Verify all dropdowns pre-populate correctly
|
|
||||||
3. Verify Year dropdown shows selected year
|
|
||||||
4. Verify Make dropdown shows selected make
|
|
||||||
5. Verify Model dropdown shows selected model
|
|
||||||
6. Verify Trim dropdown shows selected trim
|
|
||||||
7. Verify Engine dropdown shows selected engine
|
|
||||||
8. Verify Transmission dropdown shows selected transmission
|
|
||||||
9. Try changing year - verify cascade resets correctly
|
|
||||||
10. Try changing make - verify models reload
|
|
||||||
11. Submit without changes - verify no errors
|
|
||||||
12. Make a change and submit - verify update saves
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All fields pre-populate correctly
|
|
||||||
- ✅ Dropdowns show selected values
|
|
||||||
- ✅ Cascade resets work correctly when changing upstream values
|
|
||||||
- ✅ Updates save correctly
|
|
||||||
|
|
||||||
### Test 3.3: VIN Decode Feature
|
|
||||||
|
|
||||||
**Manual Test Steps**:
|
|
||||||
1. Navigate to Create Vehicle page
|
|
||||||
2. Enter a valid 17-character VIN
|
|
||||||
3. Click "Decode VIN" button
|
|
||||||
4. Verify loading state shows
|
|
||||||
5. Verify fields auto-populate (year, make, model, etc.)
|
|
||||||
6. Verify dropdowns cascade correctly after decode
|
|
||||||
7. Verify all dropdowns show correct options
|
|
||||||
8. Verify can change values after decode
|
|
||||||
9. Submit form - verify vehicle creates correctly
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ VIN decode still works
|
|
||||||
- ✅ Auto-population triggers cascade correctly
|
|
||||||
- ✅ No console errors
|
|
||||||
- ✅ Can modify after decode
|
|
||||||
|
|
||||||
### Test 3.4: Electric Vehicle Selection
|
|
||||||
|
|
||||||
**Manual Test Steps**:
|
|
||||||
1. Create vehicle form
|
|
||||||
2. Select year: 2024
|
|
||||||
3. Select make: "Tesla"
|
|
||||||
4. Select model: "Model 3"
|
|
||||||
5. Select trim: "Long Range" (or available trim)
|
|
||||||
6. Verify engine dropdown shows "N/A (Electric)"
|
|
||||||
7. Verify can still select transmission
|
|
||||||
8. Submit form
|
|
||||||
9. Verify vehicle saves with "N/A (Electric)" engine
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Electric vehicles selectable
|
|
||||||
- ✅ Engine shows "N/A (Electric)"
|
|
||||||
- ✅ Form submission works
|
|
||||||
- ✅ Data saves correctly
|
|
||||||
|
|
||||||
### Test 3.5: Edge Cases
|
|
||||||
|
|
||||||
**Test: Make/Model with Spaces**
|
|
||||||
1. Select "Land Rover" from makes
|
|
||||||
2. Verify models load correctly
|
|
||||||
3. Verify no URL encoding issues
|
|
||||||
|
|
||||||
**Test: Rapid Selection Changes**
|
|
||||||
1. Select year quickly
|
|
||||||
2. Immediately change to different year
|
|
||||||
3. Verify no race conditions
|
|
||||||
4. Verify correct data loads
|
|
||||||
|
|
||||||
**Test: Empty Dropdowns**
|
|
||||||
1. Find a year/make/model combination with no data
|
|
||||||
2. Verify "No options available" message
|
|
||||||
3. Verify form handles gracefully
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Special characters handled
|
|
||||||
- ✅ No race conditions
|
|
||||||
- ✅ Empty states handled gracefully
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Mobile Tests (REQUIRED per CLAUDE.md)
|
|
||||||
|
|
||||||
**ALL features MUST be tested on mobile.**
|
|
||||||
|
|
||||||
### Test 4.1: Mobile Create Form
|
|
||||||
|
|
||||||
**Test on mobile viewport** (< 768px width):
|
|
||||||
1. Open create vehicle form
|
|
||||||
2. Verify dropdowns render correctly
|
|
||||||
3. Verify touch interactions work
|
|
||||||
4. Verify loading states visible
|
|
||||||
5. Verify no horizontal scrolling
|
|
||||||
6. Complete full vehicle creation flow
|
|
||||||
7. Verify form submits successfully
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Layout responsive on mobile
|
|
||||||
- ✅ Dropdowns usable with touch
|
|
||||||
- ✅ No layout overflow
|
|
||||||
- ✅ All functionality works on mobile
|
|
||||||
|
|
||||||
### Test 4.2: Mobile Edit Form
|
|
||||||
|
|
||||||
**Test on mobile viewport**:
|
|
||||||
1. Open edit vehicle form
|
|
||||||
2. Verify pre-populated data displays correctly
|
|
||||||
3. Verify can scroll through all fields
|
|
||||||
4. Make changes using touch interface
|
|
||||||
5. Submit form
|
|
||||||
6. Verify updates save
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Edit form works on mobile
|
|
||||||
- ✅ Touch interactions smooth
|
|
||||||
- ✅ Form submission works
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Regression Tests
|
|
||||||
|
|
||||||
Ensure existing features still work:
|
|
||||||
|
|
||||||
### Test 5.1: Vehicle List View
|
|
||||||
|
|
||||||
1. Navigate to vehicles list
|
|
||||||
2. Verify all vehicles display correctly
|
|
||||||
3. Verify vehicle details show correct data
|
|
||||||
4. Verify old vehicles (created before migration) display correctly
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Vehicle list works
|
|
||||||
- ✅ Old vehicles display correctly
|
|
||||||
- ✅ No data migration issues
|
|
||||||
|
|
||||||
### Test 5.2: Vehicle Detail View
|
|
||||||
|
|
||||||
1. Click on a vehicle to view details
|
|
||||||
2. Verify all fields display correctly
|
|
||||||
3. Verify make/model/engine/transmission show as strings
|
|
||||||
4. Verify can navigate to edit
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ Detail view works
|
|
||||||
- ✅ All data displays correctly
|
|
||||||
|
|
||||||
### Test 5.3: VIN Decode Functionality
|
|
||||||
|
|
||||||
1. Test VIN decode in create form
|
|
||||||
2. Test VIN decode with various VINs
|
|
||||||
3. Verify accuracy of decoded data
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ VIN decode still works
|
|
||||||
- ✅ Accuracy maintained
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Performance Tests
|
|
||||||
|
|
||||||
### Test 6.1: Database Query Performance
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run EXPLAIN ANALYZE on critical queries
|
|
||||||
docker exec mvp-postgres psql -U postgres -d motovaultpro <<EOF
|
|
||||||
EXPLAIN ANALYZE SELECT DISTINCT make FROM vehicle_options WHERE year = 2024;
|
|
||||||
EXPLAIN ANALYZE SELECT DISTINCT model FROM vehicle_options WHERE year = 2024 AND make = 'Ford';
|
|
||||||
EXPLAIN ANALYZE SELECT DISTINCT trim FROM vehicle_options WHERE year = 2024 AND make = 'Ford' AND model = 'F-150';
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All queries < 50ms execution time
|
|
||||||
- ✅ Indexes being used (check query plans)
|
|
||||||
|
|
||||||
### Test 6.2: API Response Times
|
|
||||||
|
|
||||||
Use browser dev tools Network tab:
|
|
||||||
1. Measure /dropdown/years response time
|
|
||||||
2. Measure /dropdown/makes response time
|
|
||||||
3. Measure /dropdown/models response time
|
|
||||||
4. Measure /dropdown/trims response time
|
|
||||||
5. Measure /dropdown/engines response time
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ All API calls < 200ms
|
|
||||||
- ✅ No performance regressions vs old system
|
|
||||||
|
|
||||||
### Test 6.3: Frontend Rendering Performance
|
|
||||||
|
|
||||||
1. Open create form
|
|
||||||
2. Measure time to first dropdown interaction
|
|
||||||
3. Measure cascade load times
|
|
||||||
4. Verify no UI freezing
|
|
||||||
5. Check for memory leaks (long session test)
|
|
||||||
|
|
||||||
**Pass Criteria**:
|
|
||||||
- ✅ UI remains responsive
|
|
||||||
- ✅ No memory leaks
|
|
||||||
- ✅ Cascade loads feel instant (< 500ms)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Code Quality Tests (Per CLAUDE.md)
|
|
||||||
|
|
||||||
### Test 7.1: Automated Checks
|
|
||||||
|
|
||||||
**Backend**:
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
|
|
||||||
# TypeScript compilation
|
|
||||||
npm run build
|
|
||||||
# Expected: No errors
|
|
||||||
|
|
||||||
# Linting
|
|
||||||
npm run lint
|
|
||||||
# Expected: No errors
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
npm test
|
|
||||||
# Expected: All tests pass
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend**:
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
|
|
||||||
# TypeScript compilation
|
|
||||||
npm run build
|
|
||||||
# Expected: No errors
|
|
||||||
|
|
||||||
# Linting
|
|
||||||
npm run lint
|
|
||||||
# Expected: No errors
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
npm test
|
|
||||||
# Expected: All tests pass
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pass Criteria** (per CLAUDE.md):
|
|
||||||
- ✅ **ALL linters pass with zero issues**
|
|
||||||
- ✅ **ALL tests pass**
|
|
||||||
- ✅ **No errors, no formatting issues, no linting problems**
|
|
||||||
- ✅ **Zero tolerance - everything must be green**
|
|
||||||
|
|
||||||
### Test 7.2: Code Review Checklist
|
|
||||||
|
|
||||||
- [ ] No commented-out code (old code deleted per CLAUDE.md)
|
|
||||||
- [ ] Meaningful variable names used
|
|
||||||
- [ ] No unused imports
|
|
||||||
- [ ] No console.log statements (use logger)
|
|
||||||
- [ ] Error handling present in all async functions
|
|
||||||
- [ ] TypeScript types are explicit (no 'any')
|
|
||||||
- [ ] Documentation comments updated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Integration Test Script
|
|
||||||
|
|
||||||
Create an automated test script:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// backend/src/features/vehicles/tests/integration/dropdown-migration.test.ts
|
|
||||||
|
|
||||||
describe('Vehicle Dropdown Migration Integration Tests', () => {
|
|
||||||
describe('Database Layer', () => {
|
|
||||||
it('should have correct record counts', async () => {
|
|
||||||
const result = await pool.query('SELECT COUNT(*) FROM vehicle_options');
|
|
||||||
expect(parseInt(result.rows[0].count)).toBe(1122644);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return string arrays from functions', async () => {
|
|
||||||
const result = await pool.query('SELECT * FROM get_makes_for_year(2024)');
|
|
||||||
expect(Array.isArray(result.rows)).toBe(true);
|
|
||||||
expect(typeof result.rows[0].make).toBe('string');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('API Layer', () => {
|
|
||||||
it('should return string arrays for all dropdown endpoints', async () => {
|
|
||||||
const makes = await vehiclesApi.getMakes(2024);
|
|
||||||
expect(Array.isArray(makes)).toBe(true);
|
|
||||||
expect(typeof makes[0]).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle special characters in parameters', async () => {
|
|
||||||
const models = await vehiclesApi.getModels(2024, 'Land Rover');
|
|
||||||
expect(Array.isArray(models)).toBe(true);
|
|
||||||
expect(models.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return real transmission data (not hardcoded)', async () => {
|
|
||||||
const transmissions = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
|
|
||||||
expect(transmissions).not.toContain('Automatic'); // Old hardcoded value
|
|
||||||
expect(transmissions.some(t => t.includes('Speed'))).toBe(true); // Real data
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Electric Vehicles', () => {
|
|
||||||
it('should handle NULL engine_id gracefully', async () => {
|
|
||||||
const engines = await vehiclesApi.getEngines(2024, 'Tesla', 'Model 3', 'Long Range');
|
|
||||||
expect(engines).toContain('N/A (Electric)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
cd backend && npm test -- dropdown-migration.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Final Validation Checklist
|
|
||||||
|
|
||||||
Before declaring migration complete, verify ALL items:
|
|
||||||
|
|
||||||
### Database
|
|
||||||
- [ ] All tables created successfully
|
|
||||||
- [ ] Record counts correct (30K engines, 828 transmissions, 1.1M+ vehicles)
|
|
||||||
- [ ] Data quality verified (Title Case names, correct year range)
|
|
||||||
- [ ] Database functions operational
|
|
||||||
- [ ] Query performance < 50ms
|
|
||||||
- [ ] Indexes created and being used
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- [ ] All repository methods return string[]
|
|
||||||
- [ ] All service methods use string parameters
|
|
||||||
- [ ] All controller endpoints accept string query params
|
|
||||||
- [ ] Transmissions return real data (not hardcoded)
|
|
||||||
- [ ] Error handling works correctly
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] All linters pass (ZERO errors - per CLAUDE.md)
|
|
||||||
- [ ] All backend tests pass
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- [ ] API client uses string parameters
|
|
||||||
- [ ] API client returns string[]
|
|
||||||
- [ ] Form component simplified (no ID lookups)
|
|
||||||
- [ ] Create mode works end-to-end
|
|
||||||
- [ ] Edit mode pre-populates correctly
|
|
||||||
- [ ] VIN decode works correctly
|
|
||||||
- [ ] Electric vehicles display correctly
|
|
||||||
- [ ] TypeScript compiles with no errors
|
|
||||||
- [ ] All linters pass (ZERO errors - per CLAUDE.md)
|
|
||||||
- [ ] All frontend tests pass
|
|
||||||
|
|
||||||
### Mobile (REQUIRED)
|
|
||||||
- [ ] Create form works on mobile
|
|
||||||
- [ ] Edit form works on mobile
|
|
||||||
- [ ] Touch interactions smooth
|
|
||||||
- [ ] No layout issues
|
|
||||||
- [ ] Form submission works on mobile
|
|
||||||
|
|
||||||
### Regression
|
|
||||||
- [ ] Vehicle list view works
|
|
||||||
- [ ] Vehicle detail view works
|
|
||||||
- [ ] Existing vehicles display correctly
|
|
||||||
- [ ] No features broken
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- [ ] Database queries < 50ms
|
|
||||||
- [ ] API responses < 200ms
|
|
||||||
- [ ] UI remains responsive
|
|
||||||
- [ ] No memory leaks
|
|
||||||
|
|
||||||
### Code Quality (Per CLAUDE.md)
|
|
||||||
- [ ] **ALL linters pass with ZERO issues**
|
|
||||||
- [ ] **ALL tests pass**
|
|
||||||
- [ ] **No formatting errors**
|
|
||||||
- [ ] **No console errors in browser**
|
|
||||||
- [ ] Old code deleted (not commented out)
|
|
||||||
- [ ] Documentation updated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent 7 (Testing & Validation): COMPLETE
|
|
||||||
|
|
||||||
Test Results Summary:
|
|
||||||
✓ Database: All data migrated successfully (1.1M+ records)
|
|
||||||
✓ Database: Query performance < 50ms (meets requirements)
|
|
||||||
✓ Backend API: All endpoints return string[] format
|
|
||||||
✓ Backend API: String parameters handled correctly
|
|
||||||
✓ Backend API: Transmissions return real data (verified)
|
|
||||||
✓ Backend API: Electric vehicles handled correctly
|
|
||||||
✓ Frontend: Create form works end-to-end
|
|
||||||
✓ Frontend: Edit form pre-populates correctly
|
|
||||||
✓ Frontend: VIN decode functionality preserved
|
|
||||||
✓ Mobile: All features tested and working on mobile
|
|
||||||
✓ Performance: No regressions, meets performance targets
|
|
||||||
✓ Code Quality: ALL linters pass with ZERO errors (per CLAUDE.md)
|
|
||||||
✓ Code Quality: ALL tests pass
|
|
||||||
✓ Regression: No existing features broken
|
|
||||||
|
|
||||||
Issues Found: [None OR list any issues]
|
|
||||||
|
|
||||||
Migration Status: ✅ READY FOR PRODUCTION
|
|
||||||
|
|
||||||
The vehicle dropdown system has been successfully migrated from ID-based
|
|
||||||
to string-based architecture with comprehensive test coverage.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version**: 1.0
|
|
||||||
**Last Updated**: 2025-11-10
|
|
||||||
**Status**: Ready for Implementation
|
|
||||||
Reference in New Issue
Block a user