From 0d90829d3144116bde9a24f2e302e9603abfb0bc Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 15 May 2026 21:46:48 -0500 Subject: [PATCH] fix: coerce decimals in fuel-logs enhanced repository methods (refs #244) The enhanced API path on FuelLogsRepository returned raw pg rows straight to the service layer, so DECIMAL columns (fuel_units, cost_per_unit, total_cost, gallons, price_per_gallon) arrived as strings instead of numbers. The service layer compensated with scattered Number() coercion in toEnhancedResponse and getVehicleStats, and EfficiencyCalculationService silently leaned on JS string-to- number coercion in division. Type signatures across the stack declared number and the runtime delivered string. Add a private mapEnhancedRow that coerces all DECIMAL columns with parseFloat while preserving snake_case keys (the convention the service layer already uses to access these rows). Apply it in every enhanced read/write path: createEnhanced, findByVehicleIdEnhanced, findByUserIdEnhanced, findByIdEnhanced, getPreviousLogByOdometer, getLatestLogForVehicle, updateEnhanced. Drop the now-redundant Number() wrappers in toEnhancedResponse and getVehicleStats. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fuel-logs/data/fuel-logs.repository.ts | 28 ++++++++++++++----- .../fuel-logs/domain/fuel-logs.service.ts | 10 +++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/src/features/fuel-logs/data/fuel-logs.repository.ts b/backend/src/features/fuel-logs/data/fuel-logs.repository.ts index f2b9501..71b3c51 100644 --- a/backend/src/features/fuel-logs/data/fuel-logs.repository.ts +++ b/backend/src/features/fuel-logs/data/fuel-logs.repository.ts @@ -247,6 +247,20 @@ export class FuelLogsRepository { }; } + // Coerces DECIMAL columns from node-postgres strings to numbers while + // preserving snake_case row keys used by the enhanced API service callers. + private mapEnhancedRow(row: any): any { + if (!row) return null; + return { + ...row, + fuel_units: row.fuel_units != null ? parseFloat(row.fuel_units) : row.fuel_units, + cost_per_unit: row.cost_per_unit != null ? parseFloat(row.cost_per_unit) : row.cost_per_unit, + total_cost: row.total_cost != null ? parseFloat(row.total_cost) : row.total_cost, + gallons: row.gallons != null ? parseFloat(row.gallons) : row.gallons, + price_per_gallon: row.price_per_gallon != null ? parseFloat(row.price_per_gallon) : row.price_per_gallon, + }; + } + // Enhanced API support (new schema) async createEnhanced(data: { userId: string; @@ -293,7 +307,7 @@ export class FuelLogsRepository { data.notes ?? null ]; const res = await this.pool.query(query, values); - return res.rows[0] ?? null; + return this.mapEnhancedRow(res.rows[0] ?? null); } async findByVehicleIdEnhanced(vehicleId: string): Promise { @@ -301,7 +315,7 @@ export class FuelLogsRepository { `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; + return res.rows.map(r => this.mapEnhancedRow(r)); } async findByUserIdEnhanced(userId: string): Promise { @@ -309,12 +323,12 @@ export class FuelLogsRepository { `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; + return res.rows.map(r => this.mapEnhancedRow(r)); } async findByIdEnhanced(id: string): Promise { const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]); - return res.rows[0] ?? null; + return this.mapEnhancedRow(res.rows[0] ?? null); } async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise { @@ -322,7 +336,7 @@ export class FuelLogsRepository { `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; + return this.mapEnhancedRow(res.rows[0] ?? null); } async getLatestLogForVehicle(vehicleId: string): Promise { @@ -330,7 +344,7 @@ export class FuelLogsRepository { `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; + return this.mapEnhancedRow(res.rows[0] ?? null); } async updateEnhanced(id: string, data: { @@ -416,6 +430,6 @@ export class FuelLogsRepository { return null; } - return result.rows[0]; + return this.mapEnhancedRow(result.rows[0]); } } diff --git a/backend/src/features/fuel-logs/domain/fuel-logs.service.ts b/backend/src/features/fuel-logs/domain/fuel-logs.service.ts index 1b70350..8d02823 100644 --- a/backend/src/features/fuel-logs/domain/fuel-logs.service.ts +++ b/backend/src/features/fuel-logs/domain/fuel-logs.service.ts @@ -234,8 +234,8 @@ export class FuelLogsService { return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels }; } - const totalFuelUnits = rows.reduce((s, r) => s + (Number(r.fuel_units) || 0), 0); - const totalCost = rows.reduce((s, r) => s + (Number(r.total_cost) || 0), 0); + const totalFuelUnits = rows.reduce((s, r) => s + (r.fuel_units || 0), 0); + const totalCost = rows.reduce((s, r) => s + (r.total_cost || 0), 0); const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0; const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime()); @@ -282,9 +282,9 @@ export class FuelLogsService { tripDistance: row.trip_distance ?? undefined, fuelType: row.fuel_type as FuelType, fuelGrade: row.fuel_grade ?? undefined, - fuelUnits: Number(row.fuel_units), - costPerUnit: Number(row.cost_per_unit), - totalCost: Number(row.total_cost), + fuelUnits: row.fuel_units ?? 0, + costPerUnit: row.cost_per_unit ?? 0, + totalCost: row.total_cost ?? 0, locationData: row.location_data ?? undefined, notes: row.notes ?? undefined, efficiency: efficiency, -- 2.49.1