From fdc34aee2fa1ba107f15ceea1e296134b0b1b258 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 15 May 2026 21:06:24 -0500 Subject: [PATCH] fix: coerce numeric/decimal columns in repository mappers (refs #241) node-postgres returns numeric/decimal columns as JavaScript strings, but the TypeScript interfaces for MaintenanceRecord and OwnershipCost declare numeric fields as number. The mappers were passing values through raw, breaking type-safe arithmetic and display (e.g. the amount column on the vehicle summary screen was empty until the recent frontend workaround in PR #240, and OwnershipCostsList silently no-ops toLocaleString on the string). Backend - mapMaintenanceRecord: coerce cost via Number() when non-null. - ownership-costs mapRow: coerce amount via Number(). Frontend (remove now-redundant workarounds) - MaintenanceRecordsList: drop Number() coercion on cost and odometerReading; use the number values directly. - VehicleDetailPage / VehicleDetailMobile: revert the PR #240 cost coercion to the simple typeof number guard now that the backend honors the type. Scope notes - Other repositories with the same pattern (stations, community-stations, fuel-logs enhanced methods) are tracked separately because they have unclear downstream consumers and warrant their own investigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../features/maintenance/data/maintenance.repository.ts | 3 ++- .../ownership-costs/data/ownership-costs.repository.ts | 3 ++- .../maintenance/components/MaintenanceRecordsList.tsx | 8 ++++---- .../src/features/vehicles/mobile/VehicleDetailMobile.tsx | 4 +--- .../src/features/vehicles/pages/VehicleDetailPage.tsx | 4 +--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/src/features/maintenance/data/maintenance.repository.ts b/backend/src/features/maintenance/data/maintenance.repository.ts index 52c948e..0c862b9 100644 --- a/backend/src/features/maintenance/data/maintenance.repository.ts +++ b/backend/src/features/maintenance/data/maintenance.repository.ts @@ -18,7 +18,8 @@ export class MaintenanceRepository { subtypes: row.subtypes, date: row.date, odometerReading: row.odometer_reading, - cost: row.cost, + // node-postgres returns numeric/decimal columns as strings; coerce to honor the number type. + cost: row.cost != null ? Number(row.cost) : undefined, shopName: row.shop_name, notes: row.notes, receiptDocumentId: row.receipt_document_id, diff --git a/backend/src/features/ownership-costs/data/ownership-costs.repository.ts b/backend/src/features/ownership-costs/data/ownership-costs.repository.ts index 6096d06..1119eb6 100644 --- a/backend/src/features/ownership-costs/data/ownership-costs.repository.ts +++ b/backend/src/features/ownership-costs/data/ownership-costs.repository.ts @@ -16,7 +16,8 @@ export class OwnershipCostsRepository { vehicleId: row.vehicle_id, documentId: row.document_id, costType: row.cost_type, - amount: row.amount, + // node-postgres returns numeric/decimal columns as strings; coerce to honor the number type. + amount: Number(row.amount), description: row.description, periodStart: row.period_start, periodEnd: row.period_end, diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx index 0bc0180..314105d 100644 --- a/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx @@ -115,16 +115,16 @@ export const MaintenanceRecordsList: React.FC = ({ {categoryDisplay} ({subtypeCount}) - {record.odometerReading && ( + {record.odometerReading != null && ( )} - {record.cost && ( + {record.cost != null && ( = ({ secondaryParts.push(new Date(rec.date).toLocaleDateString()); secondaryParts.push('Maintenance'); const secondary = secondaryParts.join(' • '); - // Backend returns numeric/decimal columns as strings via node-postgres; coerce. - const costNum = rec.cost != null ? Number(rec.cost) : NaN; - const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined; + const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined; list.push({ id: rec.id, type: 'Maintenance', diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index 4bdce5b..65e7e0f 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -148,9 +148,7 @@ export const VehicleDetailPage: React.FC = () => { if (subtypeText) parts.push(subtypeText); if (rec.shopName) parts.push(rec.shopName); const summary = parts.join(' • '); - // Backend returns numeric/decimal columns as strings via node-postgres; coerce. - const costNum = rec.cost != null ? Number(rec.cost) : NaN; - const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined; + const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined; list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount }); } }