This commit is contained in:
Eric Gullickson
2025-11-01 21:27:42 -05:00
parent 20953c6dee
commit 046c66fc7d
203 changed files with 5699 additions and 404943 deletions

View File

@@ -2,7 +2,6 @@ import { FastifyReply, FastifyRequest } from 'fastify';
import { DocumentsService } from '../domain/documents.service';
import type { CreateBody, IdParams, ListQuery, UpdateBody } from './documents.validation';
import { getStorageService } from '../../../core/storage/storage.service';
import { appConfig } from '../../../core/config/config-loader';
import { logger } from '../../../core/logging/logger';
import path from 'path';
import { Transform, TransformCallback } from 'stream';
@@ -238,7 +237,7 @@ export class DocumentsController {
mp.file.pipe(counter);
const storage = getStorageService();
const bucket = (doc.storage_bucket || appConfig.getMinioConfig().bucket);
const bucket = 'documents'; // Filesystem storage ignores bucket, but keep for interface compatibility
const version = 'v1';
const unique = cryptoRandom();
const key = `documents/${userId}/${doc.vehicle_id}/${doc.id}/${version}/${unique}.${ext}`;

View File

@@ -2,7 +2,6 @@
* @ai-summary Fastify routes for documents API
*/
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify';
import { tenantMiddleware } from '../../../core/middleware/tenant';
import { DocumentsController } from './documents.controller';
// Note: Validation uses TypeScript types at handler level; follow existing repo pattern (no JSON schema registration)
@@ -14,17 +13,17 @@ export const documentsRoutes: FastifyPluginAsync = async (
const requireAuth = fastify.authenticate.bind(fastify);
fastify.get('/documents', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.list.bind(ctrl)
});
fastify.get<{ Params: any }>('/documents/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.get.bind(ctrl)
});
fastify.get<{ Params: any }>('/documents/vehicle/:vehicleId', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: async (req, reply) => {
const userId = (req as any).user?.sub as string;
const query = { vehicleId: (req.params as any).vehicleId };
@@ -34,27 +33,27 @@ export const documentsRoutes: FastifyPluginAsync = async (
});
fastify.post<{ Body: any }>('/documents', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.create.bind(ctrl)
});
fastify.put<{ Params: any; Body: any }>('/documents/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.update.bind(ctrl)
});
fastify.delete<{ Params: any }>('/documents/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.remove.bind(ctrl)
});
fastify.post<{ Params: any }>('/documents/:id/upload', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.upload.bind(ctrl)
});
fastify.get<{ Params: any }>('/documents/:id/download', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.download.bind(ctrl)
});
};

View File

@@ -1,256 +0,0 @@
/**
* @ai-summary Unit tests for MinIO storage adapter
* @ai-context Tests storage layer with mocked MinIO client
*/
import { createMinioAdapter } from '../../../../core/storage/adapters/minio.adapter';
import { Client as MinioClient } from 'minio';
import { appConfig } from '../../../../core/config/config-loader';
import { Readable } from 'stream';
// Mock dependencies
jest.mock('minio');
jest.mock('../../../../core/config/config-loader');
const mockMinioClient = jest.mocked(MinioClient);
const mockAppConfig = jest.mocked(appConfig);
describe('MinIO Storage Adapter', () => {
let clientInstance: jest.Mocked<MinioClient>;
let adapter: ReturnType<typeof createMinioAdapter>;
beforeEach(() => {
jest.clearAllMocks();
clientInstance = {
putObject: jest.fn(),
getObject: jest.fn(),
removeObject: jest.fn(),
statObject: jest.fn(),
presignedGetObject: jest.fn(),
presignedPutObject: jest.fn(),
} as any;
mockMinioClient.mockImplementation(() => clientInstance);
mockAppConfig.getMinioConfig.mockReturnValue({
endpoint: 'localhost',
port: 9000,
accessKey: 'testkey',
secretKey: 'testsecret',
bucket: 'test-bucket',
});
adapter = createMinioAdapter();
});
describe('putObject', () => {
it('should upload Buffer with correct parameters', async () => {
const buffer = Buffer.from('test content');
clientInstance.putObject.mockResolvedValue('etag-123');
await adapter.putObject('test-bucket', 'test-key', buffer, 'text/plain', { 'x-custom': 'value' });
expect(clientInstance.putObject).toHaveBeenCalledWith(
'test-bucket',
'test-key',
buffer,
buffer.length,
{
'Content-Type': 'text/plain',
'x-custom': 'value',
}
);
});
it('should upload string with correct parameters', async () => {
const content = 'test content';
clientInstance.putObject.mockResolvedValue('etag-123');
await adapter.putObject('test-bucket', 'test-key', content, 'text/plain');
expect(clientInstance.putObject).toHaveBeenCalledWith(
'test-bucket',
'test-key',
content,
content.length,
{ 'Content-Type': 'text/plain' }
);
});
it('should upload stream without size', async () => {
const stream = new Readable();
clientInstance.putObject.mockResolvedValue('etag-123');
await adapter.putObject('test-bucket', 'test-key', stream, 'application/octet-stream');
expect(clientInstance.putObject).toHaveBeenCalledWith(
'test-bucket',
'test-key',
stream,
undefined,
{ 'Content-Type': 'application/octet-stream' }
);
});
it('should handle upload without content type', async () => {
const buffer = Buffer.from('test');
clientInstance.putObject.mockResolvedValue('etag-123');
await adapter.putObject('test-bucket', 'test-key', buffer);
expect(clientInstance.putObject).toHaveBeenCalledWith(
'test-bucket',
'test-key',
buffer,
buffer.length,
{}
);
});
});
describe('getObjectStream', () => {
it('should return object stream', async () => {
const mockStream = new Readable();
clientInstance.getObject.mockResolvedValue(mockStream);
const result = await adapter.getObjectStream('test-bucket', 'test-key');
expect(clientInstance.getObject).toHaveBeenCalledWith('test-bucket', 'test-key');
expect(result).toBe(mockStream);
});
});
describe('deleteObject', () => {
it('should remove object', async () => {
clientInstance.removeObject.mockResolvedValue(undefined);
await adapter.deleteObject('test-bucket', 'test-key');
expect(clientInstance.removeObject).toHaveBeenCalledWith('test-bucket', 'test-key');
});
});
describe('headObject', () => {
it('should return object metadata', async () => {
const mockStat = {
size: 1024,
etag: 'test-etag',
lastModified: '2024-01-01T00:00:00Z',
metaData: {
'content-type': 'application/pdf',
'x-custom-header': 'custom-value',
},
};
clientInstance.statObject.mockResolvedValue(mockStat);
const result = await adapter.headObject('test-bucket', 'test-key');
expect(clientInstance.statObject).toHaveBeenCalledWith('test-bucket', 'test-key');
expect(result).toEqual({
size: 1024,
etag: 'test-etag',
lastModified: new Date('2024-01-01T00:00:00Z'),
contentType: 'application/pdf',
metadata: mockStat.metaData,
});
});
it('should handle metadata with Content-Type header', async () => {
const mockStat = {
size: 1024,
etag: 'test-etag',
lastModified: '2024-01-01T00:00:00Z',
metaData: {
'Content-Type': 'image/jpeg',
},
};
clientInstance.statObject.mockResolvedValue(mockStat);
const result = await adapter.headObject('test-bucket', 'test-key');
expect(result.contentType).toBe('image/jpeg');
});
it('should handle missing optional fields', async () => {
const mockStat = {
size: 1024,
etag: 'test-etag',
};
clientInstance.statObject.mockResolvedValue(mockStat);
const result = await adapter.headObject('test-bucket', 'test-key');
expect(result).toEqual({
size: 1024,
etag: 'test-etag',
lastModified: undefined,
contentType: undefined,
metadata: undefined,
});
});
});
describe('getSignedUrl', () => {
it('should generate GET signed URL with default expiry', async () => {
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
const result = await adapter.getSignedUrl('test-bucket', 'test-key');
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 300);
expect(result).toBe('https://example.com/signed-url');
});
it('should generate GET signed URL with custom expiry', async () => {
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
const result = await adapter.getSignedUrl('test-bucket', 'test-key', {
method: 'GET',
expiresSeconds: 600,
});
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 600);
expect(result).toBe('https://example.com/signed-url');
});
it('should generate PUT signed URL', async () => {
clientInstance.presignedPutObject.mockResolvedValue('https://example.com/put-url');
const result = await adapter.getSignedUrl('test-bucket', 'test-key', {
method: 'PUT',
expiresSeconds: 300,
});
expect(clientInstance.presignedPutObject).toHaveBeenCalledWith('test-bucket', 'test-key', 300);
expect(result).toBe('https://example.com/put-url');
});
it('should enforce minimum expiry time', async () => {
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
await adapter.getSignedUrl('test-bucket', 'test-key', { expiresSeconds: 0 });
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 1);
});
it('should enforce maximum expiry time', async () => {
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
await adapter.getSignedUrl('test-bucket', 'test-key', { expiresSeconds: 10000000 });
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 604800); // 7 days max
});
});
describe('MinioClient instantiation', () => {
it('should create client with correct configuration', () => {
expect(mockMinioClient).toHaveBeenCalledWith({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: 'testkey',
secretKey: 'testsecret',
});
});
});
});

View File

@@ -8,7 +8,6 @@ import { FastifyPluginAsync } from 'fastify';
// Types handled in controllers; no explicit generics required here
import { FuelLogsController } from './fuel-logs.controller';
import { FuelGradeController } from './fuel-grade.controller';
import { tenantMiddleware } from '../../../core/middleware/tenant';
export const fuelLogsRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
@@ -19,53 +18,53 @@ export const fuelLogsRoutes: FastifyPluginAsync = async (
// GET /api/fuel-logs - Get user's fuel logs
fastify.get('/fuel-logs', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.getUserFuelLogs.bind(fuelLogsController)
});
// POST /api/fuel-logs - Create new fuel log
fastify.post('/fuel-logs', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.createFuelLog.bind(fuelLogsController)
});
// GET /api/fuel-logs/:id - Get specific fuel log
fastify.get('/fuel-logs/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.getFuelLog.bind(fuelLogsController)
});
// PUT /api/fuel-logs/:id - Update fuel log
fastify.put('/fuel-logs/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.updateFuelLog.bind(fuelLogsController)
});
// DELETE /api/fuel-logs/:id - Delete fuel log
fastify.delete('/fuel-logs/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.deleteFuelLog.bind(fuelLogsController)
});
// NEW ENDPOINTS under /api/fuel-logs
fastify.get('/fuel-logs/vehicle/:vehicleId', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.getFuelLogsByVehicle.bind(fuelLogsController)
});
fastify.get('/fuel-logs/vehicle/:vehicleId/stats', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelLogsController.getFuelStats.bind(fuelLogsController)
});
// Fuel type/grade discovery
fastify.get('/fuel-logs/fuel-types', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelGradeController.getAllFuelTypes.bind(fuelGradeController)
});
fastify.get('/fuel-logs/fuel-grades/:fuelType', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: fuelGradeController.getFuelGrades.bind(fuelGradeController)
});
};

View File

@@ -2,7 +2,6 @@
* @ai-summary Fastify routes for maintenance API
*/
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify';
import { tenantMiddleware } from '../../../core/middleware/tenant';
import { MaintenanceController } from './maintenance.controller';
export const maintenanceRoutes: FastifyPluginAsync = async (
@@ -14,64 +13,64 @@ export const maintenanceRoutes: FastifyPluginAsync = async (
// Maintenance Records
fastify.get('/maintenance/records', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.listRecords.bind(ctrl)
});
fastify.get<{ Params: any }>('/maintenance/records/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.getRecord.bind(ctrl)
});
fastify.get<{ Params: any }>('/maintenance/records/vehicle/:vehicleId', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.getRecordsByVehicle.bind(ctrl)
});
fastify.post<{ Body: any }>('/maintenance/records', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.createRecord.bind(ctrl)
});
fastify.put<{ Params: any; Body: any }>('/maintenance/records/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.updateRecord.bind(ctrl)
});
fastify.delete<{ Params: any }>('/maintenance/records/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.deleteRecord.bind(ctrl)
});
// Maintenance Schedules
fastify.get<{ Params: any }>('/maintenance/schedules/vehicle/:vehicleId', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.getSchedulesByVehicle.bind(ctrl)
});
fastify.post<{ Body: any }>('/maintenance/schedules', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.createSchedule.bind(ctrl)
});
fastify.put<{ Params: any; Body: any }>('/maintenance/schedules/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.updateSchedule.bind(ctrl)
});
fastify.delete<{ Params: any }>('/maintenance/schedules/:id', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.deleteSchedule.bind(ctrl)
});
// Utility Routes
fastify.get<{ Params: { vehicleId: string }; Querystring: { currentMileage?: string } }>('/maintenance/upcoming/:vehicleId', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.getUpcoming.bind(ctrl)
});
fastify.get<{ Params: any }>('/maintenance/subtypes/:category', {
preHandler: [requireAuth, tenantMiddleware as any],
preHandler: [requireAuth],
handler: ctrl.getSubtypes.bind(ctrl)
});
};

View File

@@ -11,7 +11,6 @@ import {
StationParams
} from '../domain/stations.types';
import { StationsController } from './stations.controller';
import { tenantMiddleware } from '../../../core/middleware/tenant';
export const stationsRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
@@ -21,25 +20,25 @@ export const stationsRoutes: FastifyPluginAsync = async (
// POST /api/stations/search - Search nearby stations
fastify.post<{ Body: StationSearchBody }>('/stations/search', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: stationsController.searchStations.bind(stationsController)
});
// POST /api/stations/save - Save a station to user's favorites
fastify.post<{ Body: SaveStationBody }>('/stations/save', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: stationsController.saveStation.bind(stationsController)
});
// GET /api/stations/saved - Get user's saved stations
fastify.get('/stations/saved', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: stationsController.getSavedStations.bind(stationsController)
});
// DELETE /api/stations/saved/:placeId - Remove saved station
fastify.delete<{ Params: StationParams }>('/stations/saved/:placeId', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: stationsController.removeSavedStation.bind(stationsController)
});
};

View File

@@ -1,95 +0,0 @@
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
import axios from 'axios';
import { tenantMiddleware } from '../../core/middleware/tenant';
import { getTenantConfig } from '../../core/config/tenant';
import { logger } from '../../core/logging/logger';
export const tenantManagementRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
const baseUrl = getTenantConfig().platformServicesUrl;
// Require JWT on all routes
const requireAuth = fastify.authenticate.bind(fastify);
// Admin-only guard using tenant context from middleware
const requireAdmin = async (request: any, reply: any) => {
if (request.tenantId !== 'admin') {
reply.code(403).send({ error: 'Admin access required' });
return;
}
};
const forwardAuthHeader = (request: any) => {
const auth = request.headers['authorization'];
return auth ? { Authorization: auth as string } : {};
};
// List all tenants
fastify.get('/api/admin/tenants', { preHandler: [requireAuth, tenantMiddleware as any, requireAdmin] }, async (request, reply) => {
try {
const resp = await axios.get(`${baseUrl}/api/v1/tenants`, {
headers: forwardAuthHeader(request),
});
return reply.code(200).send(resp.data);
} catch (error: any) {
logger.error('Failed to list tenants', { error: error?.message });
return reply.code(error?.response?.status || 500).send(error?.response?.data || { error: 'Failed to list tenants' });
}
});
// Create new tenant
fastify.post('/api/admin/tenants', { preHandler: [requireAuth, tenantMiddleware as any, requireAdmin] }, async (request: any, reply) => {
try {
const resp = await axios.post(`${baseUrl}/api/v1/tenants`, request.body, {
headers: { ...forwardAuthHeader(request), 'Content-Type': 'application/json' },
});
return reply.code(201).send(resp.data);
} catch (error: any) {
logger.error('Failed to create tenant', { error: error?.message });
return reply.code(error?.response?.status || 500).send(error?.response?.data || { error: 'Failed to create tenant' });
}
});
// List pending signups for a tenant
fastify.get('/api/admin/tenants/:tenantId/signups', { preHandler: [requireAuth, tenantMiddleware as any, requireAdmin] }, async (request: any, reply) => {
try {
const { tenantId } = request.params;
const resp = await axios.get(`${baseUrl}/api/v1/tenants/${encodeURIComponent(tenantId)}/signups`, {
headers: forwardAuthHeader(request),
});
return reply.code(200).send(resp.data);
} catch (error: any) {
logger.error('Failed to list signups', { error: error?.message });
return reply.code(error?.response?.status || 500).send(error?.response?.data || { error: 'Failed to list signups' });
}
});
// Approve signup
fastify.put('/api/admin/signups/:signupId/approve', { preHandler: [requireAuth, tenantMiddleware as any, requireAdmin] }, async (request: any, reply) => {
try {
const { signupId } = request.params;
const resp = await axios.put(`${baseUrl}/api/v1/signups/${encodeURIComponent(signupId)}/approve`, {}, {
headers: forwardAuthHeader(request),
});
return reply.code(200).send(resp.data);
} catch (error: any) {
logger.error('Failed to approve signup', { error: error?.message });
return reply.code(error?.response?.status || 500).send(error?.response?.data || { error: 'Failed to approve signup' });
}
});
// Reject signup
fastify.put('/api/admin/signups/:signupId/reject', { preHandler: [requireAuth, tenantMiddleware as any, requireAdmin] }, async (request: any, reply) => {
try {
const { signupId } = request.params;
const resp = await axios.put(`${baseUrl}/api/v1/signups/${encodeURIComponent(signupId)}/reject`, {}, {
headers: forwardAuthHeader(request),
});
return reply.code(200).send(resp.data);
} catch (error: any) {
logger.error('Failed to reject signup', { error: error?.message });
return reply.code(error?.response?.status || 500).send(error?.response?.data || { error: 'Failed to reject signup' });
}
});
};
export default tenantManagementRoutes;

View File

@@ -228,7 +228,7 @@ npm test -- features/vehicles --coverage
- Both features depend on vehicles as primary entity
### Potential Enhancements
- Vehicle image uploads (MinIO integration)
- Vehicle image uploads (filesystem storage integration)
- Enhanced platform service integration for real-time updates
- Vehicle value estimation via additional platform services
- Maintenance scheduling based on vehicle age/mileage

View File

@@ -11,7 +11,6 @@ import {
VehicleParams
} from '../domain/vehicles.types';
import { VehiclesController } from './vehicles.controller';
import { tenantMiddleware } from '../../../core/middleware/tenant';
export const vehiclesRoutes: FastifyPluginAsync = async (
fastify: FastifyInstance,
@@ -21,31 +20,31 @@ export const vehiclesRoutes: FastifyPluginAsync = async (
// GET /api/vehicles - Get user's vehicles
fastify.get('/vehicles', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getUserVehicles.bind(vehiclesController)
});
// POST /api/vehicles - Create new vehicle
fastify.post<{ Body: CreateVehicleBody }>('/vehicles', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.createVehicle.bind(vehiclesController)
});
// GET /api/vehicles/:id - Get specific vehicle
fastify.get<{ Params: VehicleParams }>('/vehicles/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getVehicle.bind(vehiclesController)
});
// PUT /api/vehicles/:id - Update vehicle
fastify.put<{ Params: VehicleParams; Body: UpdateVehicleBody }>('/vehicles/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.updateVehicle.bind(vehiclesController)
});
// DELETE /api/vehicles/:id - Delete vehicle
fastify.delete<{ Params: VehicleParams }>('/vehicles/:id', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.deleteVehicle.bind(vehiclesController)
});
@@ -53,43 +52,43 @@ export const vehiclesRoutes: FastifyPluginAsync = async (
// GET /api/vehicles/dropdown/years - Available model years
fastify.get('/vehicles/dropdown/years', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownYears.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/makes?year=2024 - Get makes for year (Level 1)
fastify.get<{ Querystring: { year: number } }>('/vehicles/dropdown/makes', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownMakes.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/models?year=2024&make_id=1 - Get models for year/make (Level 2)
fastify.get<{ Querystring: { year: number; make_id: number } }>('/vehicles/dropdown/models', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownModels.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/trims?year=2024&make_id=1&model_id=1 - Get trims (Level 3)
fastify.get<{ Querystring: { year: number; make_id: number; model_id: number } }>('/vehicles/dropdown/trims', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownTrims.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/engines?year=2024&make_id=1&model_id=1&trim_id=1 - Get engines (Level 4)
fastify.get<{ Querystring: { year: number; make_id: number; model_id: number; trim_id: number } }>('/vehicles/dropdown/engines', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownEngines.bind(vehiclesController)
});
// GET /api/vehicles/dropdown/transmissions?year=2024&make_id=1&model_id=1 - Get transmissions (Level 3)
fastify.get<{ Querystring: { year: number; make_id: number; model_id: number } }>('/vehicles/dropdown/transmissions', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController)
});
// POST /api/vehicles/decode-vin - Decode VIN and return vehicle information
fastify.post<{ Body: { vin: string } }>('/vehicles/decode-vin', {
preHandler: [fastify.authenticate, tenantMiddleware],
preHandler: [fastify.authenticate],
handler: vehiclesController.decodeVIN.bind(vehiclesController)
});
};

View File

@@ -26,11 +26,9 @@ export class VehiclesService {
constructor(private repository: VehiclesRepository) {
// Initialize platform vehicles client
const platformConfig = appConfig.getPlatformServiceConfig('vehicles');
const platformVehiclesUrl = appConfig.getPlatformVehiclesUrl();
const platformClient = new PlatformVehiclesClient({
baseURL: platformConfig.url,
apiKey: platformConfig.apiKey,
tenantId: appConfig.config.server.tenant_id,
baseURL: platformVehiclesUrl,
timeout: 3000,
logger
});

View File

@@ -44,8 +44,6 @@ export interface VINDecodeResponse {
export interface PlatformVehiclesClientConfig {
baseURL: string;
apiKey: string;
tenantId?: string;
timeout?: number;
logger?: Logger;
}
@@ -58,27 +56,19 @@ export class PlatformVehiclesClient {
private readonly httpClient: AxiosInstance;
private readonly logger: Logger | undefined;
private readonly circuitBreakers: Map<string, CircuitBreaker> = new Map();
private readonly tenantId: string | undefined;
constructor(config: PlatformVehiclesClientConfig) {
this.logger = config.logger;
this.tenantId = config.tenantId || process.env.TENANT_ID;
// Initialize HTTP client
this.httpClient = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 3000,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
});
// Inject tenant header for all requests when available
if (this.tenantId) {
this.httpClient.defaults.headers.common['X-Tenant-ID'] = this.tenantId;
}
// Setup response interceptors for logging
this.httpClient.interceptors.response.use(
(response) => {