MVP Build
This commit is contained in:
35
backend/src/features/fuel-logs/README.md
Normal file
35
backend/src/features/fuel-logs/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# UfuelUlogs Feature Capsule
|
||||
|
||||
## Quick Summary (50 tokens)
|
||||
[AI: Complete feature description, main operations, dependencies, caching strategy]
|
||||
|
||||
## API Endpoints
|
||||
- GET /api/fuel-logs - List all fuel-logs
|
||||
- GET /api/fuel-logs/:id - Get specific lUfuelUlogs
|
||||
- POST /api/fuel-logs - Create new lUfuelUlogs
|
||||
- PUT /api/fuel-logs/:id - Update lUfuelUlogs
|
||||
- DELETE /api/fuel-logs/:id - Delete lUfuelUlogs
|
||||
|
||||
## Structure
|
||||
- **api/** - HTTP endpoints, routes, validators
|
||||
- **domain/** - Business logic, types, rules
|
||||
- **data/** - Repository, database queries
|
||||
- **migrations/** - Feature-specific schema
|
||||
- **external/** - External API integrations
|
||||
- **events/** - Event handlers
|
||||
- **tests/** - All feature tests
|
||||
- **docs/** - Detailed documentation
|
||||
|
||||
## Dependencies
|
||||
- Internal: core/auth, core/cache
|
||||
- External: [List any external APIs]
|
||||
- Database: fuel-logs table
|
||||
|
||||
## Quick Commands
|
||||
```bash
|
||||
# Run feature tests
|
||||
npm test -- features/fuel-logs
|
||||
|
||||
# Run feature migrations
|
||||
npm run migrate:feature fuel-logs
|
||||
```
|
||||
186
backend/src/features/fuel-logs/api/fuel-logs.controller.ts
Normal file
186
backend/src/features/fuel-logs/api/fuel-logs.controller.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* @ai-summary HTTP request handlers for fuel logs
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { FuelLogsService } from '../domain/fuel-logs.service';
|
||||
import { validateCreateFuelLog, validateUpdateFuelLog } from './fuel-logs.validators';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
|
||||
export class FuelLogsController {
|
||||
constructor(private service: FuelLogsService) {}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const validation = validateCreateFuelLog(req.body);
|
||||
if (!validation.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: validation.error.errors
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.createFuelLog(validation.data, userId);
|
||||
res.status(201).json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating fuel log', { error: error.message });
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('Unauthorized')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
listByVehicle = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { vehicleId } = req.params;
|
||||
const result = await this.service.getFuelLogsByVehicle(vehicleId, userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error listing fuel logs', { error: error.message });
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('Unauthorized')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
listAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const result = await this.service.getUserFuelLogs(userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error listing all fuel logs', { error: error.message });
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await this.service.getFuelLog(id, userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting fuel log', { error: error.message });
|
||||
|
||||
if (error.message === 'Fuel log not found') {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const validation = validateUpdateFuelLog(req.body);
|
||||
if (!validation.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: validation.error.errors
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.updateFuelLog(id, validation.data, userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error updating fuel log', { error: error.message });
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
await this.service.deleteFuelLog(id, userId);
|
||||
res.status(204).send();
|
||||
} catch (error: any) {
|
||||
logger.error('Error deleting fuel log', { error: error.message });
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
getStats = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { vehicleId } = req.params;
|
||||
const result = await this.service.getVehicleStats(vehicleId, userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting fuel stats', { error: error.message });
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message === 'Unauthorized') {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
backend/src/features/fuel-logs/api/fuel-logs.routes.ts
Normal file
32
backend/src/features/fuel-logs/api/fuel-logs.routes.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @ai-summary Route definitions for fuel logs API
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { FuelLogsController } from './fuel-logs.controller';
|
||||
import { FuelLogsService } from '../domain/fuel-logs.service';
|
||||
import { FuelLogsRepository } from '../data/fuel-logs.repository';
|
||||
import { authMiddleware } from '../../../core/security/auth.middleware';
|
||||
import pool from '../../../core/config/database';
|
||||
|
||||
export function registerFuelLogsRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Initialize layers
|
||||
const repository = new FuelLogsRepository(pool);
|
||||
const service = new FuelLogsService(repository);
|
||||
const controller = new FuelLogsController(service);
|
||||
|
||||
// Define routes
|
||||
router.get('/api/fuel-logs', authMiddleware, controller.listAll);
|
||||
router.get('/api/fuel-logs/:id', authMiddleware, controller.get);
|
||||
router.post('/api/fuel-logs', authMiddleware, controller.create);
|
||||
router.put('/api/fuel-logs/:id', authMiddleware, controller.update);
|
||||
router.delete('/api/fuel-logs/:id', authMiddleware, controller.delete);
|
||||
|
||||
// Vehicle-specific routes
|
||||
router.get('/api/vehicles/:vehicleId/fuel-logs', authMiddleware, controller.listByVehicle);
|
||||
router.get('/api/vehicles/:vehicleId/fuel-stats', authMiddleware, controller.getStats);
|
||||
|
||||
return router;
|
||||
}
|
||||
38
backend/src/features/fuel-logs/api/fuel-logs.validators.ts
Normal file
38
backend/src/features/fuel-logs/api/fuel-logs.validators.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @ai-summary Input validation for fuel logs API
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createFuelLogSchema = z.object({
|
||||
vehicleId: z.string().uuid(),
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
odometer: z.number().int().positive(),
|
||||
gallons: z.number().positive(),
|
||||
pricePerGallon: z.number().positive(),
|
||||
totalCost: z.number().positive(),
|
||||
station: z.string().max(200).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
notes: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
export const updateFuelLogSchema = z.object({
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
||||
odometer: z.number().int().positive().optional(),
|
||||
gallons: z.number().positive().optional(),
|
||||
pricePerGallon: z.number().positive().optional(),
|
||||
totalCost: z.number().positive().optional(),
|
||||
station: z.string().max(200).optional(),
|
||||
location: z.string().max(200).optional(),
|
||||
notes: z.string().max(1000).optional(),
|
||||
}).refine(data => Object.keys(data).length > 0, {
|
||||
message: 'At least one field must be provided for update'
|
||||
});
|
||||
|
||||
export function validateCreateFuelLog(data: unknown) {
|
||||
return createFuelLogSchema.safeParse(data);
|
||||
}
|
||||
|
||||
export function validateUpdateFuelLog(data: unknown) {
|
||||
return updateFuelLogSchema.safeParse(data);
|
||||
}
|
||||
249
backend/src/features/fuel-logs/domain/fuel-logs.service.ts
Normal file
249
backend/src/features/fuel-logs/domain/fuel-logs.service.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* @ai-summary Business logic for fuel logs feature
|
||||
* @ai-context Handles MPG calculations and vehicle validation
|
||||
*/
|
||||
|
||||
import { FuelLogsRepository } from '../data/fuel-logs.repository';
|
||||
import {
|
||||
FuelLog,
|
||||
CreateFuelLogRequest,
|
||||
UpdateFuelLogRequest,
|
||||
FuelLogResponse,
|
||||
FuelStats
|
||||
} from './fuel-logs.types';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import { cacheService } from '../../../core/config/redis';
|
||||
import { pool } from '../../../core/config/database';
|
||||
|
||||
export class FuelLogsService {
|
||||
private readonly cachePrefix = 'fuel-logs';
|
||||
private readonly cacheTTL = 300; // 5 minutes
|
||||
|
||||
constructor(private repository: FuelLogsRepository) {}
|
||||
|
||||
async createFuelLog(data: CreateFuelLogRequest, userId: string): Promise<FuelLogResponse> {
|
||||
logger.info('Creating fuel log', { userId, vehicleId: data.vehicleId });
|
||||
|
||||
// Verify vehicle ownership
|
||||
const vehicleCheck = await pool.query(
|
||||
'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2',
|
||||
[data.vehicleId, userId]
|
||||
);
|
||||
|
||||
if (vehicleCheck.rows.length === 0) {
|
||||
throw new Error('Vehicle not found or unauthorized');
|
||||
}
|
||||
|
||||
// Calculate MPG based on previous log
|
||||
let mpg: number | undefined;
|
||||
const previousLog = await this.repository.getPreviousLog(
|
||||
data.vehicleId,
|
||||
data.date,
|
||||
data.odometer
|
||||
);
|
||||
|
||||
if (previousLog && previousLog.odometer < data.odometer) {
|
||||
const milesDriven = data.odometer - previousLog.odometer;
|
||||
mpg = milesDriven / data.gallons;
|
||||
}
|
||||
|
||||
// Create fuel log
|
||||
const fuelLog = await this.repository.create({
|
||||
...data,
|
||||
userId,
|
||||
mpg
|
||||
});
|
||||
|
||||
// Update vehicle odometer
|
||||
await pool.query(
|
||||
'UPDATE vehicles SET odometer_reading = $1 WHERE id = $2 AND odometer_reading < $1',
|
||||
[data.odometer, data.vehicleId]
|
||||
);
|
||||
|
||||
// Invalidate caches
|
||||
await this.invalidateCaches(userId, data.vehicleId);
|
||||
|
||||
return this.toResponse(fuelLog);
|
||||
}
|
||||
|
||||
async getFuelLogsByVehicle(vehicleId: string, userId: string): Promise<FuelLogResponse[]> {
|
||||
// Verify vehicle ownership
|
||||
const vehicleCheck = await pool.query(
|
||||
'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2',
|
||||
[vehicleId, userId]
|
||||
);
|
||||
|
||||
if (vehicleCheck.rows.length === 0) {
|
||||
throw new Error('Vehicle not found or unauthorized');
|
||||
}
|
||||
|
||||
const cacheKey = `${this.cachePrefix}:vehicle:${vehicleId}`;
|
||||
|
||||
// Check cache
|
||||
const cached = await cacheService.get<FuelLogResponse[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Get from database
|
||||
const logs = await this.repository.findByVehicleId(vehicleId);
|
||||
const response = logs.map(log => this.toResponse(log));
|
||||
|
||||
// Cache result
|
||||
await cacheService.set(cacheKey, response, this.cacheTTL);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getUserFuelLogs(userId: string): Promise<FuelLogResponse[]> {
|
||||
const cacheKey = `${this.cachePrefix}:user:${userId}`;
|
||||
|
||||
// Check cache
|
||||
const cached = await cacheService.get<FuelLogResponse[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Get from database
|
||||
const logs = await this.repository.findByUserId(userId);
|
||||
const response = logs.map(log => this.toResponse(log));
|
||||
|
||||
// Cache result
|
||||
await cacheService.set(cacheKey, response, this.cacheTTL);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getFuelLog(id: string, userId: string): Promise<FuelLogResponse> {
|
||||
const log = await this.repository.findById(id);
|
||||
|
||||
if (!log) {
|
||||
throw new Error('Fuel log not found');
|
||||
}
|
||||
|
||||
if (log.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return this.toResponse(log);
|
||||
}
|
||||
|
||||
async updateFuelLog(
|
||||
id: string,
|
||||
data: UpdateFuelLogRequest,
|
||||
userId: string
|
||||
): Promise<FuelLogResponse> {
|
||||
// Verify ownership
|
||||
const existing = await this.repository.findById(id);
|
||||
if (!existing) {
|
||||
throw new Error('Fuel log not found');
|
||||
}
|
||||
if (existing.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
// Recalculate MPG if odometer or gallons changed
|
||||
let mpg = existing.mpg;
|
||||
if (data.odometer || data.gallons) {
|
||||
const previousLog = await this.repository.getPreviousLog(
|
||||
existing.vehicleId,
|
||||
data.date || existing.date.toISOString(),
|
||||
data.odometer || existing.odometer
|
||||
);
|
||||
|
||||
if (previousLog) {
|
||||
const odometer = data.odometer || existing.odometer;
|
||||
const gallons = data.gallons || existing.gallons;
|
||||
const milesDriven = odometer - previousLog.odometer;
|
||||
mpg = milesDriven / gallons;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update data with proper types
|
||||
const updateData: Partial<FuelLog> = {
|
||||
...data,
|
||||
date: data.date ? new Date(data.date) : undefined,
|
||||
mpg
|
||||
};
|
||||
|
||||
// Update
|
||||
const updated = await this.repository.update(id, updateData);
|
||||
if (!updated) {
|
||||
throw new Error('Update failed');
|
||||
}
|
||||
|
||||
// Invalidate caches
|
||||
await this.invalidateCaches(userId, existing.vehicleId);
|
||||
|
||||
return this.toResponse(updated);
|
||||
}
|
||||
|
||||
async deleteFuelLog(id: string, userId: string): Promise<void> {
|
||||
// Verify ownership
|
||||
const existing = await this.repository.findById(id);
|
||||
if (!existing) {
|
||||
throw new Error('Fuel log not found');
|
||||
}
|
||||
if (existing.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
await this.repository.delete(id);
|
||||
|
||||
// Invalidate caches
|
||||
await this.invalidateCaches(userId, existing.vehicleId);
|
||||
}
|
||||
|
||||
async getVehicleStats(vehicleId: string, userId: string): Promise<FuelStats> {
|
||||
// Verify vehicle ownership
|
||||
const vehicleCheck = await pool.query(
|
||||
'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2',
|
||||
[vehicleId, userId]
|
||||
);
|
||||
|
||||
if (vehicleCheck.rows.length === 0) {
|
||||
throw new Error('Vehicle not found or unauthorized');
|
||||
}
|
||||
|
||||
const stats = await this.repository.getStats(vehicleId);
|
||||
|
||||
if (!stats) {
|
||||
return {
|
||||
logCount: 0,
|
||||
totalGallons: 0,
|
||||
totalCost: 0,
|
||||
averagePricePerGallon: 0,
|
||||
averageMPG: 0,
|
||||
totalMiles: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private async invalidateCaches(userId: string, vehicleId: string): Promise<void> {
|
||||
await Promise.all([
|
||||
cacheService.del(`${this.cachePrefix}:user:${userId}`),
|
||||
cacheService.del(`${this.cachePrefix}:vehicle:${vehicleId}`)
|
||||
]);
|
||||
}
|
||||
|
||||
private toResponse(log: FuelLog): FuelLogResponse {
|
||||
return {
|
||||
id: log.id,
|
||||
userId: log.userId,
|
||||
vehicleId: log.vehicleId,
|
||||
date: log.date.toISOString().split('T')[0],
|
||||
odometer: log.odometer,
|
||||
gallons: log.gallons,
|
||||
pricePerGallon: log.pricePerGallon,
|
||||
totalCost: log.totalCost,
|
||||
station: log.station,
|
||||
location: log.location,
|
||||
notes: log.notes,
|
||||
mpg: log.mpg,
|
||||
createdAt: log.createdAt.toISOString(),
|
||||
updatedAt: log.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
70
backend/src/features/fuel-logs/domain/fuel-logs.types.ts
Normal file
70
backend/src/features/fuel-logs/domain/fuel-logs.types.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @ai-summary Type definitions for fuel logs feature
|
||||
* @ai-context Tracks fuel purchases and calculates MPG
|
||||
*/
|
||||
|
||||
export interface FuelLog {
|
||||
id: string;
|
||||
userId: string;
|
||||
vehicleId: string;
|
||||
date: Date;
|
||||
odometer: number;
|
||||
gallons: number;
|
||||
pricePerGallon: number;
|
||||
totalCost: number;
|
||||
station?: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
mpg?: number; // Calculated field
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateFuelLogRequest {
|
||||
vehicleId: string;
|
||||
date: string; // ISO date string
|
||||
odometer: number;
|
||||
gallons: number;
|
||||
pricePerGallon: number;
|
||||
totalCost: number;
|
||||
station?: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateFuelLogRequest {
|
||||
date?: string;
|
||||
odometer?: number;
|
||||
gallons?: number;
|
||||
pricePerGallon?: number;
|
||||
totalCost?: number;
|
||||
station?: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface FuelLogResponse {
|
||||
id: string;
|
||||
userId: string;
|
||||
vehicleId: string;
|
||||
date: string;
|
||||
odometer: number;
|
||||
gallons: number;
|
||||
pricePerGallon: number;
|
||||
totalCost: number;
|
||||
station?: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
mpg?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface FuelStats {
|
||||
totalGallons: number;
|
||||
totalCost: number;
|
||||
averagePricePerGallon: number;
|
||||
averageMPG: number;
|
||||
totalMiles: number;
|
||||
logCount: number;
|
||||
}
|
||||
18
backend/src/features/fuel-logs/index.ts
Normal file
18
backend/src/features/fuel-logs/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @ai-summary Public API for fuel-logs feature capsule
|
||||
*/
|
||||
|
||||
// Export service for use by other features
|
||||
export { FuelLogsService } from './domain/fuel-logs.service';
|
||||
|
||||
// Export types
|
||||
export type {
|
||||
FuelLog,
|
||||
CreateFuelLogRequest,
|
||||
UpdateFuelLogRequest,
|
||||
FuelLogResponse,
|
||||
FuelStats
|
||||
} from './domain/fuel-logs.types';
|
||||
|
||||
// Internal: Register routes
|
||||
export { registerFuelLogsRoutes } from './api/fuel-logs.routes';
|
||||
@@ -0,0 +1,34 @@
|
||||
-- Create fuel_logs table
|
||||
CREATE TABLE IF NOT EXISTS fuel_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
vehicle_id UUID NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
odometer INTEGER NOT NULL,
|
||||
gallons DECIMAL(10, 3) NOT NULL,
|
||||
price_per_gallon DECIMAL(10, 3) NOT NULL,
|
||||
total_cost DECIMAL(10, 2) NOT NULL,
|
||||
station VARCHAR(200),
|
||||
location VARCHAR(200),
|
||||
notes TEXT,
|
||||
mpg DECIMAL(10, 2), -- Calculated based on previous log
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_fuel_logs_vehicle
|
||||
FOREIGN KEY (vehicle_id)
|
||||
REFERENCES vehicles(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX idx_fuel_logs_user_id ON fuel_logs(user_id);
|
||||
CREATE INDEX idx_fuel_logs_vehicle_id ON fuel_logs(vehicle_id);
|
||||
CREATE INDEX idx_fuel_logs_date ON fuel_logs(date DESC);
|
||||
CREATE INDEX idx_fuel_logs_created_at ON fuel_logs(created_at DESC);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE TRIGGER update_fuel_logs_updated_at
|
||||
BEFORE UPDATE ON fuel_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
Reference in New Issue
Block a user