MVP Build
This commit is contained in:
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user