Pre-web changes

This commit is contained in:
Eric Gullickson
2025-11-05 11:04:48 -06:00
parent 45fea0f307
commit 0c3ed01f4b
25 changed files with 257 additions and 3538 deletions

View File

@@ -13,16 +13,17 @@ Primary entity for vehicle management consuming MVP Platform Vehicles Service. H
- `DELETE /api/vehicles/:id` - Soft delete vehicle
### Hierarchical Vehicle Dropdowns
**Status**: Dropdown methods are TODO stubs in vehicles service. Frontend directly consumes platform module endpoints.
**Status**: Vehicles service now proxies the platform vehicle catalog to provide fully dynamic dropdowns. Each selection step filters the next list, ensuring only valid combinations are shown.
Frontend consumes (via `/platform` module, not vehicles feature):
- `GET /api/platform/years` - Get all years
- `GET /api/platform/makes?year={year}` - Get makes for year
- `GET /api/platform/models?year={year}&make_id={make_id}` - Get models for make/year
- `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}` - Get trims
- `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}` - Get engines
- `GET /api/platform/transmissions?year={year}&make_id={make_id}&model_id={model_id}` - Get transmissions
- `GET /api/platform/vehicle?vin={vin}` - Decode VIN
Sequence:
1. `GET /api/vehicles/dropdown/years``[number]` (latest to oldest).
2. `GET /api/vehicles/dropdown/makes?year={year}` `{ id, name }[]` (only makes produced in the selected year).
3. `GET /api/vehicles/dropdown/models?year={year}&make_id={id}` `{ id, name }[]` (models offered for that year/make).
4. `GET /api/vehicles/dropdown/trims?year={year}&make_id={id}&model_id={id}``{ id, name }[]` (valid trims for the chosen combination).
5. `GET /api/vehicles/dropdown/engines?year={year}&make_id={id}&model_id={id}&trim_id={id}` `{ id, name }[]` (engines tied to the trim).
6. `GET /api/vehicles/dropdown/transmissions?year={year}&make_id={id}&model_id={id}``{ id, name }[]` (static options: Automatic, Manual).
All dropdown endpoints call `Platform VehicleDataService` behind the scenes, reuse Redis caching, and return normalized `{ id, name }` payloads ready for the frontend.
## Authentication
- All vehicles endpoints (including dropdowns) require a valid JWT (Auth0).

View File

@@ -4,7 +4,7 @@
*/
import { VehiclesRepository } from '../data/vehicles.repository';
import { getVINDecodeService, getPool } from '../../platform';
import { getVINDecodeService, getVehicleDataService, getPool } from '../../platform';
import {
Vehicle,
CreateVehicleRequest,
@@ -162,52 +162,52 @@ export class VehiclesService {
await cacheService.del(cacheKey);
}
async getDropdownMakes(_year: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService
// For now, return empty array to allow migration to complete
logger.warn('Dropdown makes not yet implemented via platform feature');
return [];
async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown makes via platform module', { year });
return vehicleDataService.getMakes(pool, year);
}
async getDropdownModels(_year: number, _makeId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown models not yet implemented via platform feature');
return [];
async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown models via platform module', { year, makeId });
return vehicleDataService.getModels(pool, year, makeId);
}
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown transmissions not yet implemented via platform feature');
return [];
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> {
logger.info('Providing dropdown transmissions from static list');
return [
{ id: 1, name: 'Automatic' },
{ id: 2, name: 'Manual' }
];
}
async getDropdownEngines(_year: number, _makeId: number, _modelId: number, _trimId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown engines not yet implemented via platform feature');
return [];
async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown engines via platform module', { year, makeId, modelId, trimId });
return vehicleDataService.getEngines(pool, year, modelId, trimId);
}
async getDropdownTrims(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> {
// TODO: Implement using platform VehicleDataService
logger.warn('Dropdown trims not yet implemented via platform feature');
return [];
async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown trims via platform module', { year, makeId, modelId });
return vehicleDataService.getTrims(pool, year, modelId);
}
async getDropdownYears(): Promise<number[]> {
try {
logger.info('Getting dropdown years');
// Fallback: generate recent years
const currentYear = new Date().getFullYear();
const years: number[] = [];
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
return years;
} catch (error) {
logger.error('Failed to get dropdown years', { error });
const currentYear = new Date().getFullYear();
const years: number[] = [];
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
return years;
}
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown years via platform module');
return vehicleDataService.getYears(pool);
}
async decodeVIN(vin: string): Promise<{

View File

@@ -12,20 +12,42 @@ import * as platformModule from '../../../platform';
jest.mock('../../data/vehicles.repository');
jest.mock('../../../../core/config/redis');
jest.mock('../../../platform', () => ({
getVINDecodeService: jest.fn()
getVINDecodeService: jest.fn(),
getVehicleDataService: jest.fn(),
getPool: jest.fn()
}));
const mockRepository = jest.mocked(VehiclesRepository);
const mockCacheService = jest.mocked(cacheService);
const mockGetVINDecodeService = jest.mocked(platformModule.getVINDecodeService);
const mockGetVehicleDataService = jest.mocked(platformModule.getVehicleDataService);
const mockGetPool = jest.mocked(platformModule.getPool);
describe('VehiclesService', () => {
let service: VehiclesService;
let repositoryInstance: jest.Mocked<VehiclesRepository>;
let vehicleDataServiceMock: {
getYears: jest.Mock;
getMakes: jest.Mock;
getModels: jest.Mock;
getTrims: jest.Mock;
getEngines: jest.Mock;
};
beforeEach(() => {
jest.clearAllMocks();
vehicleDataServiceMock = {
getYears: jest.fn(),
getMakes: jest.fn(),
getModels: jest.fn(),
getTrims: jest.fn(),
getEngines: jest.fn(),
};
mockGetVehicleDataService.mockReturnValue(vehicleDataServiceMock as any);
mockGetPool.mockReturnValue('mock-pool' as any);
repositoryInstance = {
create: jest.fn(),
findByUserId: jest.fn(),
@@ -39,6 +61,63 @@ describe('VehiclesService', () => {
service = new VehiclesService(repositoryInstance);
});
describe('dropdown data integration', () => {
it('retrieves years from platform service', async () => {
vehicleDataServiceMock.getYears.mockResolvedValue([2024, 2023]);
const result = await service.getDropdownYears();
expect(mockGetVehicleDataService).toHaveBeenCalled();
expect(vehicleDataServiceMock.getYears).toHaveBeenCalledWith('mock-pool');
expect(result).toEqual([2024, 2023]);
});
it('retrieves makes scoped to year', async () => {
vehicleDataServiceMock.getMakes.mockResolvedValue([{ id: 10, name: 'Honda' }]);
const result = await service.getDropdownMakes(2024);
expect(vehicleDataServiceMock.getMakes).toHaveBeenCalledWith('mock-pool', 2024);
expect(result).toEqual([{ id: 10, name: 'Honda' }]);
});
it('retrieves models scoped to year and make', async () => {
vehicleDataServiceMock.getModels.mockResolvedValue([{ id: 20, name: 'Civic' }]);
const result = await service.getDropdownModels(2024, 10);
expect(vehicleDataServiceMock.getModels).toHaveBeenCalledWith('mock-pool', 2024, 10);
expect(result).toEqual([{ id: 20, name: 'Civic' }]);
});
it('retrieves trims scoped to year, make, and model', async () => {
vehicleDataServiceMock.getTrims.mockResolvedValue([{ id: 30, name: 'Sport' }]);
const result = await service.getDropdownTrims(2024, 10, 20);
expect(vehicleDataServiceMock.getTrims).toHaveBeenCalledWith('mock-pool', 2024, 20);
expect(result).toEqual([{ id: 30, name: 'Sport' }]);
});
it('retrieves engines scoped to selection', async () => {
vehicleDataServiceMock.getEngines.mockResolvedValue([{ id: 40, name: '2.0L Turbo' }]);
const result = await service.getDropdownEngines(2024, 10, 20, 30);
expect(vehicleDataServiceMock.getEngines).toHaveBeenCalledWith('mock-pool', 2024, 20, 30);
expect(result).toEqual([{ id: 40, name: '2.0L Turbo' }]);
});
it('returns static transmission options', async () => {
const result = await service.getDropdownTransmissions(2024, 10, 20);
expect(result).toEqual([
{ id: 1, name: 'Automatic' },
{ id: 2, name: 'Manual' }
]);
});
});
});
describe('createVehicle', () => {
const mockVehicleData = {
vin: '1HGBH41JXMN109186',
@@ -94,7 +173,7 @@ describe('VehiclesService', () => {
const result = await service.createVehicle(mockVehicleData, 'user-123');
expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186');
expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('1HGBH41JXMN109186');
expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('mock-pool', '1HGBH41JXMN109186');
expect(repositoryInstance.create).toHaveBeenCalledWith({
...mockVehicleData,
userId: 'user-123',
@@ -319,4 +398,4 @@ describe('VehiclesService', () => {
await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized');
});
});
});
});