Compare commits

...

2 Commits

Author SHA1 Message Date
0dd5746f60 Merge pull request 'fix: coerce numeric/decimal columns in repository mappers (#241)' (#242) from issue-241-numeric-mapper-coercion into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 10s
Deploy to Staging / Deploy to Staging (push) Successful in 41s
Deploy to Staging / Verify Staging (push) Successful in 3s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Reviewed-on: #242
2026-05-16 02:35:02 +00:00
Eric Gullickson
fdc34aee2f fix: coerce numeric/decimal columns in repository mappers (refs #241)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 1m49s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 43s
Deploy to Staging / Verify Staging (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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) <noreply@anthropic.com>
2026-05-15 21:06:24 -05:00
5 changed files with 10 additions and 12 deletions

View File

@@ -18,7 +18,8 @@ export class MaintenanceRepository {
subtypes: row.subtypes, subtypes: row.subtypes,
date: row.date, date: row.date,
odometerReading: row.odometer_reading, 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, shopName: row.shop_name,
notes: row.notes, notes: row.notes,
receiptDocumentId: row.receipt_document_id, receiptDocumentId: row.receipt_document_id,

View File

@@ -16,7 +16,8 @@ export class OwnershipCostsRepository {
vehicleId: row.vehicle_id, vehicleId: row.vehicle_id,
documentId: row.document_id, documentId: row.document_id,
costType: row.cost_type, 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, description: row.description,
periodStart: row.period_start, periodStart: row.period_start,
periodEnd: row.period_end, periodEnd: row.period_end,

View File

@@ -115,16 +115,16 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
{categoryDisplay} ({subtypeCount}) {categoryDisplay} ({subtypeCount})
</Typography> </Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}> <Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}>
{record.odometerReading && ( {record.odometerReading != null && (
<Chip <Chip
label={`${Number(record.odometerReading).toLocaleString()} miles`} label={`${record.odometerReading.toLocaleString()} miles`}
size="small" size="small"
variant="outlined" variant="outlined"
/> />
)} )}
{record.cost && ( {record.cost != null && (
<Chip <Chip
label={`$${Number(record.cost).toFixed(2)}`} label={`$${record.cost.toFixed(2)}`}
size="small" size="small"
color="primary" color="primary"
variant="outlined" variant="outlined"

View File

@@ -142,9 +142,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
secondaryParts.push(new Date(rec.date).toLocaleDateString()); secondaryParts.push(new Date(rec.date).toLocaleDateString());
secondaryParts.push('Maintenance'); secondaryParts.push('Maintenance');
const secondary = secondaryParts.join(' • '); const secondary = secondaryParts.join(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce. const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined;
list.push({ list.push({
id: rec.id, id: rec.id,
type: 'Maintenance', type: 'Maintenance',

View File

@@ -148,9 +148,7 @@ export const VehicleDetailPage: React.FC = () => {
if (subtypeText) parts.push(subtypeText); if (subtypeText) parts.push(subtypeText);
if (rec.shopName) parts.push(rec.shopName); if (rec.shopName) parts.push(rec.shopName);
const summary = parts.join(' • '); const summary = parts.join(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce. const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined;
list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount }); list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount });
} }
} }