backend/src/features/maintenance/data/maintenance.repository.tsmapMaintenanceRecord() (and mapMaintenanceSchedule()) pass numeric columns straight through from the raw pg row. node-postgres returns numeric/decimal columns as strings, but the TypeScript types declare them as number. This forces every frontend consumer to defensively coerce with Number() or risk silent failures like the one fixed in #239 (the maintenance amount column rendering as dashes because typeof rec.cost === 'number' was false).
Affected Fields
mapMaintenanceRecord (maintenance_records table)
cost — typed number, returned as string
odometerReading — typed number, returned as string (suspected — same column type, not yet confirmed at runtime)
(All *Mileage and interval fields are likely numeric/integer in pg and should be confirmed against the migration.)
Evidence
frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx:127 already does Number(record.cost).toFixed(2) — a workaround that hides the underlying bug.
PR #240 added the same Number() coercion in the vehicle summary screen consumers because the cost column was rendering empty without it. That coercion is now in two more places and should be temporary.
Expected Behavior
The mapper should honor its declared TypeScript types. Numeric columns should be coerced once, in the mapper:
…and equivalent for the schedule mapper's numeric fields.
After the mapper fix, the defensive Number() calls in MaintenanceRecordsList.tsx, VehicleDetailPage.tsx, and VehicleDetailMobile.tsx can be removed (or left as belt-and-suspenders — but the bug at the source is gone).
Investigation Areas
Confirm pg column types for all suspected fields against the maintenance migration in backend/src/features/maintenance/ (or backend/migrations/).
Check whether other feature repositories (fuel-logs, ownership-costs, documents) have the same issue. Fuel logs currently display totalCost as a number without frontend coercion — verify whether that's because the mapper coerces or because the column type is different.
Consider a small utility helper in backend/src/core/db/ (e.g. toNumber()) that all repos can use for numeric coercion, so this pattern is consistent.
Verify mappers stay aligned with CLAUDE.md's "Repository Pattern for Case Conversion" — the mapper is the right place for type conversion too, not just case conversion.
Acceptance Criteria
mapMaintenanceRecord and mapMaintenanceSchedule coerce all numeric fields to number (or null) before returning.
Other repository mappers audited for the same issue; fixes applied where the type declares number but the column is numeric/decimal.
API responses for /maintenance/records* return numeric fields as JSON numbers, not strings (verify with curl or network tab).
Defensive Number() coercions in maintenance frontend components (MaintenanceRecordsList, VehicleDetailPage, VehicleDetailMobile) can be removed without regressions.
Linting, type-check, and tests pass.
Mobile + desktop verification of the maintenance cost columns and any other numeric displays affected.
Out of Scope
Schema migration changes (this is a serialization-layer bug, not a column-type bug).
Fixing every feature's mapper in one PR — if multiple features are affected, decompose into sub-issues per feature.
Related
#239 / PR #240 — surfaced this bug; works around it with frontend coercion in two new consumers.
## Summary
`backend/src/features/maintenance/data/maintenance.repository.ts` `mapMaintenanceRecord()` (and `mapMaintenanceSchedule()`) pass numeric columns straight through from the raw pg row. node-postgres returns `numeric`/`decimal` columns as **strings**, but the TypeScript types declare them as `number`. This forces every frontend consumer to defensively coerce with `Number()` or risk silent failures like the one fixed in #239 (the maintenance amount column rendering as dashes because `typeof rec.cost === 'number'` was false).
## Affected Fields
### `mapMaintenanceRecord` (maintenance_records table)
- `cost` — typed `number`, returned as string
- `odometerReading` — typed `number`, returned as string (suspected — same column type, not yet confirmed at runtime)
### `mapMaintenanceSchedule` (maintenance_schedules table)
- `intervalMonths` — typed `number`
- `intervalMiles` — typed `number`
- `lastServiceMileage` — typed `number`
- `nextDueMileage` — typed `number`
(All `*Mileage` and interval fields are likely `numeric`/`integer` in pg and should be confirmed against the migration.)
## Evidence
- `frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx:127` already does `Number(record.cost).toFixed(2)` — a workaround that hides the underlying bug.
- PR #240 added the same `Number()` coercion in the vehicle summary screen consumers because the cost column was rendering empty without it. That coercion is now in two more places and should be temporary.
## Expected Behavior
The mapper should honor its declared TypeScript types. Numeric columns should be coerced once, in the mapper:
```typescript
cost: row.cost != null ? Number(row.cost) : null,
odometerReading: row.odometer_reading != null ? Number(row.odometer_reading) : null,
```
…and equivalent for the schedule mapper's numeric fields.
After the mapper fix, the defensive `Number()` calls in `MaintenanceRecordsList.tsx`, `VehicleDetailPage.tsx`, and `VehicleDetailMobile.tsx` can be removed (or left as belt-and-suspenders — but the bug at the source is gone).
## Investigation Areas
- Confirm pg column types for all suspected fields against the maintenance migration in `backend/src/features/maintenance/` (or `backend/migrations/`).
- Check whether other feature repositories (`fuel-logs`, `ownership-costs`, `documents`) have the same issue. Fuel logs currently display `totalCost` as a number without frontend coercion — verify whether that's because the mapper coerces or because the column type is different.
- Consider a small utility helper in `backend/src/core/db/` (e.g. `toNumber()`) that all repos can use for numeric coercion, so this pattern is consistent.
- Verify mappers stay aligned with CLAUDE.md's "Repository Pattern for Case Conversion" — the mapper is the right place for *type* conversion too, not just case conversion.
## Acceptance Criteria
- [ ] `mapMaintenanceRecord` and `mapMaintenanceSchedule` coerce all numeric fields to `number` (or null) before returning.
- [ ] Other repository mappers audited for the same issue; fixes applied where the type declares `number` but the column is `numeric`/`decimal`.
- [ ] API responses for `/maintenance/records*` return numeric fields as JSON numbers, not strings (verify with `curl` or network tab).
- [ ] Defensive `Number()` coercions in maintenance frontend components (`MaintenanceRecordsList`, `VehicleDetailPage`, `VehicleDetailMobile`) can be removed without regressions.
- [ ] Linting, type-check, and tests pass.
- [ ] Mobile + desktop verification of the maintenance cost columns and any other numeric displays affected.
## Out of Scope
- Schema migration changes (this is a serialization-layer bug, not a column-type bug).
- Fixing every feature's mapper in one PR — if multiple features are affected, decompose into sub-issues per feature.
## Related
- #239 / PR #240 — surfaced this bug; works around it with frontend coercion in two new consumers.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
backend/src/features/maintenance/data/maintenance.repository.tsmapMaintenanceRecord()(andmapMaintenanceSchedule()) pass numeric columns straight through from the raw pg row. node-postgres returnsnumeric/decimalcolumns as strings, but the TypeScript types declare them asnumber. This forces every frontend consumer to defensively coerce withNumber()or risk silent failures like the one fixed in #239 (the maintenance amount column rendering as dashes becausetypeof rec.cost === 'number'was false).Affected Fields
mapMaintenanceRecord(maintenance_records table)cost— typednumber, returned as stringodometerReading— typednumber, returned as string (suspected — same column type, not yet confirmed at runtime)mapMaintenanceSchedule(maintenance_schedules table)intervalMonths— typednumberintervalMiles— typednumberlastServiceMileage— typednumbernextDueMileage— typednumber(All
*Mileageand interval fields are likelynumeric/integerin pg and should be confirmed against the migration.)Evidence
frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx:127already doesNumber(record.cost).toFixed(2)— a workaround that hides the underlying bug.Number()coercion in the vehicle summary screen consumers because the cost column was rendering empty without it. That coercion is now in two more places and should be temporary.Expected Behavior
The mapper should honor its declared TypeScript types. Numeric columns should be coerced once, in the mapper:
…and equivalent for the schedule mapper's numeric fields.
After the mapper fix, the defensive
Number()calls inMaintenanceRecordsList.tsx,VehicleDetailPage.tsx, andVehicleDetailMobile.tsxcan be removed (or left as belt-and-suspenders — but the bug at the source is gone).Investigation Areas
backend/src/features/maintenance/(orbackend/migrations/).fuel-logs,ownership-costs,documents) have the same issue. Fuel logs currently displaytotalCostas a number without frontend coercion — verify whether that's because the mapper coerces or because the column type is different.backend/src/core/db/(e.g.toNumber()) that all repos can use for numeric coercion, so this pattern is consistent.Acceptance Criteria
mapMaintenanceRecordandmapMaintenanceSchedulecoerce all numeric fields tonumber(or null) before returning.numberbut the column isnumeric/decimal./maintenance/records*return numeric fields as JSON numbers, not strings (verify withcurlor network tab).Number()coercions in maintenance frontend components (MaintenanceRecordsList,VehicleDetailPage,VehicleDetailMobile) can be removed without regressions.Out of Scope
Related