376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
/**
|
|
* @ai-summary Data access layer for fuel logs
|
|
* @ai-context Handles database operations and MPG calculations
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import { FuelLog, CreateFuelLogRequest, FuelStats } from '../domain/fuel-logs.types';
|
|
|
|
export class FuelLogsRepository {
|
|
constructor(private pool: Pool) {}
|
|
|
|
async create(data: CreateFuelLogRequest & { userId: string, mpg?: number }): Promise<FuelLog> {
|
|
const query = `
|
|
INSERT INTO fuel_logs (
|
|
user_id, vehicle_id, date, odometer, gallons,
|
|
price_per_gallon, total_cost, station, location, notes
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING *
|
|
`;
|
|
|
|
const values = [
|
|
data.userId,
|
|
data.vehicleId,
|
|
data.date,
|
|
data.odometer,
|
|
data.gallons,
|
|
data.pricePerGallon,
|
|
data.totalCost,
|
|
data.station,
|
|
data.location,
|
|
data.notes
|
|
];
|
|
|
|
const result = await this.pool.query(query, values);
|
|
return this.mapRow(result.rows[0]);
|
|
}
|
|
|
|
async findByVehicleId(vehicleId: string): Promise<FuelLog[]> {
|
|
const query = `
|
|
SELECT * FROM fuel_logs
|
|
WHERE vehicle_id = $1
|
|
ORDER BY date DESC, created_at DESC
|
|
`;
|
|
|
|
const result = await this.pool.query(query, [vehicleId]);
|
|
return result.rows.map(row => this.mapRow(row));
|
|
}
|
|
|
|
async findByUserId(userId: string): Promise<FuelLog[]> {
|
|
const query = `
|
|
SELECT * FROM fuel_logs
|
|
WHERE user_id = $1
|
|
ORDER BY date DESC, created_at DESC
|
|
`;
|
|
|
|
const result = await this.pool.query(query, [userId]);
|
|
return result.rows.map(row => this.mapRow(row));
|
|
}
|
|
|
|
async findById(id: string): Promise<FuelLog | null> {
|
|
const query = 'SELECT * FROM fuel_logs WHERE id = $1';
|
|
const result = await this.pool.query(query, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return this.mapRow(result.rows[0]);
|
|
}
|
|
|
|
async getPreviousLog(vehicleId: string, date: string, odometer: number): Promise<FuelLog | null> {
|
|
const query = `
|
|
SELECT * FROM fuel_logs
|
|
WHERE vehicle_id = $1
|
|
AND (date < $2 OR (date = $2 AND odometer < $3))
|
|
ORDER BY date DESC, odometer DESC
|
|
LIMIT 1
|
|
`;
|
|
|
|
const result = await this.pool.query(query, [vehicleId, date, odometer]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return this.mapRow(result.rows[0]);
|
|
}
|
|
|
|
async update(id: string, data: Partial<FuelLog>): Promise<FuelLog | null> {
|
|
const fields = [];
|
|
const values = [];
|
|
let paramCount = 1;
|
|
|
|
// Build dynamic update query
|
|
if (data.date !== undefined) {
|
|
fields.push(`date = $${paramCount++}`);
|
|
values.push(data.date);
|
|
}
|
|
if (data.odometer !== undefined) {
|
|
fields.push(`odometer = $${paramCount++}`);
|
|
values.push(data.odometer);
|
|
}
|
|
if (data.gallons !== undefined) {
|
|
fields.push(`gallons = $${paramCount++}`);
|
|
values.push(data.gallons);
|
|
}
|
|
if (data.pricePerGallon !== undefined) {
|
|
fields.push(`price_per_gallon = $${paramCount++}`);
|
|
values.push(data.pricePerGallon);
|
|
}
|
|
if (data.totalCost !== undefined) {
|
|
fields.push(`total_cost = $${paramCount++}`);
|
|
values.push(data.totalCost);
|
|
}
|
|
if (data.station !== undefined) {
|
|
fields.push(`station = $${paramCount++}`);
|
|
values.push(data.station);
|
|
}
|
|
if (data.location !== undefined) {
|
|
fields.push(`location = $${paramCount++}`);
|
|
values.push(data.location);
|
|
}
|
|
if (data.notes !== undefined) {
|
|
fields.push(`notes = $${paramCount++}`);
|
|
values.push(data.notes);
|
|
}
|
|
// mpg column removed; efficiency is computed dynamically
|
|
|
|
if (fields.length === 0) {
|
|
return this.findById(id);
|
|
}
|
|
|
|
values.push(id);
|
|
const query = `
|
|
UPDATE fuel_logs
|
|
SET ${fields.join(', ')}
|
|
WHERE id = $${paramCount}
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.pool.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return this.mapRow(result.rows[0]);
|
|
}
|
|
|
|
async delete(id: string): Promise<boolean> {
|
|
const query = 'DELETE FROM fuel_logs WHERE id = $1';
|
|
const result = await this.pool.query(query, [id]);
|
|
return (result.rowCount ?? 0) > 0;
|
|
}
|
|
|
|
async getStats(vehicleId: string): Promise<FuelStats | null> {
|
|
const query = `
|
|
SELECT
|
|
COUNT(*) as log_count,
|
|
SUM(gallons) as total_gallons,
|
|
SUM(total_cost) as total_cost,
|
|
AVG(price_per_gallon) as avg_price_per_gallon,
|
|
MAX(odometer) - MIN(odometer) as total_miles
|
|
FROM fuel_logs
|
|
WHERE vehicle_id = $1
|
|
`;
|
|
|
|
const result = await this.pool.query(query, [vehicleId]);
|
|
|
|
if (result.rows.length === 0 || result.rows[0].log_count === '0') {
|
|
return null;
|
|
}
|
|
|
|
const row = result.rows[0];
|
|
return {
|
|
logCount: parseInt(row.log_count),
|
|
totalGallons: parseFloat(row.total_gallons) || 0,
|
|
totalCost: parseFloat(row.total_cost) || 0,
|
|
averagePricePerGallon: parseFloat(row.avg_price_per_gallon) || 0,
|
|
averageMPG: 0,
|
|
totalMiles: parseInt(row.total_miles) || 0,
|
|
};
|
|
}
|
|
|
|
private mapRow(row: any): FuelLog {
|
|
return {
|
|
id: row.id,
|
|
userId: row.user_id,
|
|
vehicleId: row.vehicle_id,
|
|
date: row.date,
|
|
odometer: row.odometer,
|
|
gallons: parseFloat(row.gallons),
|
|
pricePerGallon: parseFloat(row.price_per_gallon),
|
|
totalCost: parseFloat(row.total_cost),
|
|
station: row.station,
|
|
location: row.location,
|
|
notes: row.notes,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
};
|
|
}
|
|
|
|
// Enhanced API support (new schema)
|
|
async createEnhanced(data: {
|
|
userId: string;
|
|
vehicleId: string;
|
|
dateTime: Date;
|
|
odometerReading?: number;
|
|
tripDistance?: number;
|
|
fuelType: string;
|
|
fuelGrade?: string | null;
|
|
fuelUnits: number;
|
|
costPerUnit: number;
|
|
totalCost: number;
|
|
locationData?: any;
|
|
notes?: string;
|
|
}): Promise<any> {
|
|
const query = `
|
|
INSERT INTO fuel_logs (
|
|
user_id, vehicle_id, date, date_time, odometer, trip_distance,
|
|
fuel_type, fuel_grade, fuel_units, cost_per_unit,
|
|
gallons, price_per_gallon, total_cost, location_data, notes
|
|
)
|
|
VALUES (
|
|
$1, $2, $3, $4, $5, $6,
|
|
$7, $8, $9, $10,
|
|
$11, $12, $13, $14, $15
|
|
)
|
|
RETURNING *
|
|
`;
|
|
const values = [
|
|
data.userId,
|
|
data.vehicleId,
|
|
data.dateTime.toISOString().slice(0, 10),
|
|
data.dateTime,
|
|
data.odometerReading ?? null,
|
|
data.tripDistance ?? null,
|
|
data.fuelType,
|
|
data.fuelGrade ?? null,
|
|
data.fuelUnits,
|
|
data.costPerUnit,
|
|
data.fuelUnits, // legacy support
|
|
data.costPerUnit, // legacy support
|
|
data.totalCost,
|
|
data.locationData ?? null,
|
|
data.notes ?? null
|
|
];
|
|
const res = await this.pool.query(query, values);
|
|
return res.rows[0];
|
|
}
|
|
|
|
async findByVehicleIdEnhanced(vehicleId: string): Promise<any[]> {
|
|
const res = await this.pool.query(
|
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
|
[vehicleId]
|
|
);
|
|
return res.rows;
|
|
}
|
|
|
|
async findByUserIdEnhanced(userId: string): Promise<any[]> {
|
|
const res = await this.pool.query(
|
|
`SELECT * FROM fuel_logs WHERE user_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
|
[userId]
|
|
);
|
|
return res.rows;
|
|
}
|
|
|
|
async findByIdEnhanced(id: string): Promise<any | null> {
|
|
const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]);
|
|
return res.rows[0] || null;
|
|
}
|
|
|
|
async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise<any | null> {
|
|
const res = await this.pool.query(
|
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer IS NOT NULL AND odometer < $2 ORDER BY odometer DESC LIMIT 1`,
|
|
[vehicleId, odometerReading]
|
|
);
|
|
return res.rows[0] || null;
|
|
}
|
|
|
|
async getLatestLogForVehicle(vehicleId: string): Promise<any | null> {
|
|
const res = await this.pool.query(
|
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC LIMIT 1`,
|
|
[vehicleId]
|
|
);
|
|
return res.rows[0] || null;
|
|
}
|
|
|
|
async updateEnhanced(id: string, data: {
|
|
dateTime?: Date;
|
|
odometerReading?: number;
|
|
tripDistance?: number;
|
|
fuelType?: string;
|
|
fuelGrade?: string | null;
|
|
fuelUnits?: number;
|
|
costPerUnit?: number;
|
|
locationData?: any;
|
|
notes?: string;
|
|
}): Promise<any | null> {
|
|
const fields = [];
|
|
const values = [];
|
|
let paramCount = 1;
|
|
|
|
// Build dynamic update query for enhanced schema
|
|
if (data.dateTime !== undefined) {
|
|
fields.push(`date_time = $${paramCount++}`);
|
|
fields.push(`date = $${paramCount++}`);
|
|
values.push(data.dateTime);
|
|
values.push(data.dateTime.toISOString().slice(0, 10));
|
|
}
|
|
if (data.odometerReading !== undefined) {
|
|
fields.push(`odometer = $${paramCount++}`);
|
|
values.push(data.odometerReading);
|
|
}
|
|
if (data.tripDistance !== undefined) {
|
|
fields.push(`trip_distance = $${paramCount++}`);
|
|
values.push(data.tripDistance);
|
|
}
|
|
if (data.fuelType !== undefined) {
|
|
fields.push(`fuel_type = $${paramCount++}`);
|
|
values.push(data.fuelType);
|
|
}
|
|
if (data.fuelGrade !== undefined) {
|
|
fields.push(`fuel_grade = $${paramCount++}`);
|
|
values.push(data.fuelGrade);
|
|
}
|
|
if (data.fuelUnits !== undefined) {
|
|
fields.push(`fuel_units = $${paramCount++}`);
|
|
fields.push(`gallons = $${paramCount++}`); // legacy support
|
|
values.push(data.fuelUnits);
|
|
values.push(data.fuelUnits);
|
|
}
|
|
if (data.costPerUnit !== undefined) {
|
|
fields.push(`cost_per_unit = $${paramCount++}`);
|
|
fields.push(`price_per_gallon = $${paramCount++}`); // legacy support
|
|
values.push(data.costPerUnit);
|
|
values.push(data.costPerUnit);
|
|
}
|
|
if (data.locationData !== undefined) {
|
|
fields.push(`location_data = $${paramCount++}`);
|
|
values.push(data.locationData);
|
|
}
|
|
if (data.notes !== undefined) {
|
|
fields.push(`notes = $${paramCount++}`);
|
|
values.push(data.notes);
|
|
}
|
|
|
|
// Recalculate total cost if both fuelUnits and costPerUnit are present
|
|
if (data.fuelUnits !== undefined && data.costPerUnit !== undefined) {
|
|
fields.push(`total_cost = $${paramCount++}`);
|
|
values.push(data.fuelUnits * data.costPerUnit);
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return this.findByIdEnhanced(id);
|
|
}
|
|
|
|
values.push(id);
|
|
const query = `
|
|
UPDATE fuel_logs
|
|
SET ${fields.join(', ')}, updated_at = NOW()
|
|
WHERE id = $${paramCount}
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.pool.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
}
|