fix: maintenance repository mapper returns numeric fields as strings, violating MaintenanceRecord type contract #241

Closed
opened 2026-05-16 01:56:12 +00:00 by egullickson · 0 comments
Owner

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:

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.
  • #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.
egullickson added the
status
backlog
type
bug
labels 2026-05-16 01:56:18 +00:00
egullickson added
status
in-progress
and removed
status
backlog
labels 2026-05-16 01:59:45 +00:00
egullickson added
status
review
and removed
status
in-progress
labels 2026-05-16 02:06:59 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#241