fix: coerce decimals in fuel-logs enhanced repository methods (#244) #245
Reference in New Issue
Block a user
Delete Branch "issue-244-fuel-logs-enhanced-mapper"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #244
Summary
The enhanced API path on
FuelLogsRepositoryreturned raw pg rows straight to the service layer.node-postgresreturnsDECIMALcolumns as strings, sofuel_units,cost_per_unit,total_cost,gallons, andprice_per_gallonarrived as strings throughout the enhanced code path even though every TypeScript signature in the chain declarednumber. The service compensated with scatteredNumber()coercion intoEnhancedResponseandgetVehicleStats, andEfficiencyCalculationServicesilently leaned on JS coercion in division — works today but fragile.Pre-fix audit (full audit in #244 comment)
Critical findings before touching code:
createEnhanced,findByVehicleIdEnhanced,findByUserIdEnhanced,findByIdEnhanced,getPreviousLogByOdometer,getLatestLogForVehicle,updateEnhanced. The earlier issue body only named 3 — the audit caught the rest.r.fuel_units,r.total_cost) on the raw rows. AmapRow-style camelCase mapper would silently break every caller. The right shape is a mapper that coerces values but preserves snake_case keys..includes,.split) on these fields — onlytypeof === 'number'defensive checks and arithmetic. Coercing to numbers is safe across all consumers.EfficiencyCalculationService.calculateEfficiency(distance, fuelUnits)doesdistance / fuelUnitswith operand types declarednumberbut actually receiving strings. Worked via JS coercion; will still work cleanly with numbers.Changes
backend/src/features/fuel-logs/data/fuel-logs.repository.tsmapEnhancedRow(row)that coercesfuel_units,cost_per_unit,total_cost,gallons,price_per_gallonfrom string to number viaparseFloatwhen non-null, preserving snake_case keys for the existing service-layer convention.backend/src/features/fuel-logs/domain/fuel-logs.service.tsgetVehicleStats: dropNumber(r.fuel_units)andNumber(r.total_cost)wrappers in the reduces — the values are now real numbers.toEnhancedResponse: replaceNumber(row.fuel_units),Number(row.cost_per_unit),Number(row.total_cost)with direct access plus?? 0to preserve the existing null-tolerance semantics (Number(null) === 0).What I deliberately did NOT change
trip_distanceandodometerareINTEGERcolumns; node-postgres already returns those as numbers. TheNumber(cur.trip_distance)andNumber(cur.odometer)calls ingetVehicleStats(lines 246-249) were always belt-and-suspenders no-ops. Removing them isn't part of this bug — left alone to limit blast radius.typeof === 'number'checks inFuelStatsCard,CostCalculator, andFuelLogFormare part of a broader convention (input fields, form parsing) and not tied to this specific bug. Left untouched.Promise<any>. Introducing a typedEnhancedFuelLogRowinterface is a larger cleanup with its own review surface and belongs in a separate refactor.Test plan
npm run type-check(backend): clean.npm run lint(backend, scoped to edited files): 0 errors, 18 baselineanywarnings unchanged.npx jest --testPathPattern='fuel-logs': 2 suites failed withConfiguration file not found at /app/config/production.yml— identical to baseline onmain. Pre-existing config-loader path issue; not introduced by this PR.totalCost,fuelUnits,costPerUnit,efficiencydisplay as numbers and arithmetic in vehicle stats (/garage/vehicles/:id/statsif surfaced) is correct.Acceptance criteria (from #244)
mapRowor add a siblingmapEnhancedRow— addedmapEnhancedRowto preserve snake_case convention.createEnhanced,findByVehicleIdEnhanced,updateEnhanced(and 4 more found in the audit) with the mapper.Number()calls in consumers that become unnecessary.Number()or relied on/which auto-coerces).main.