fix: coerce numeric/decimal columns in repository mappers (#241) #242
Reference in New Issue
Block a user
Delete Branch "issue-241-numeric-mapper-coercion"
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 #241
Summary
node-postgresreturnsnumeric/decimalcolumns as JavaScript strings, but the TypeScript interfaces forMaintenanceRecordandOwnershipCostdeclare those fields asnumber. The mappers were passing the raw row through, so the runtime types didn't match the declared types — forcing every consumer to defensively callNumber()or silently break.Observable symptoms this fixes
OwnershipCostsListwas callingvalue.toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 })on a string; strings have atoLocaleStringmethod that ignores the options object, so ownership-costs amounts displayed without proper currency formatting (e.g."100.5"instead of"100.50").Changes
Backend
backend/src/features/maintenance/data/maintenance.repository.ts—mapMaintenanceRecordcoercesrow.costwithNumber()when non-null, returnsundefinedotherwise (matches the optional typecost?: number).backend/src/features/ownership-costs/data/ownership-costs.repository.ts—mapRowcoercesrow.amountwithNumber()(amount: numberis NOT NULL in the DB).Pattern matches the existing
vehicles.repository.tsconvention which already coercespurchasePrice,insuranceCost, andregistrationCost.Frontend (remove now-redundant workarounds)
frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx— dropNumber(record.cost).toFixed(2)andNumber(record.odometerReading).toLocaleString(); the values are now real numbers. Tightened the truthiness check to!= nullso a legitimate0still renders.frontend/src/features/vehicles/pages/VehicleDetailPage.tsxandVehicleDetailMobile.tsx— revert the PR #240 cost coercion back to the simpletypeof rec.cost === 'number'guard. The backend now honors the type contract, so the workaround is unnecessary.Scope decisions / what is NOT in this PR
The audit (recorded in the issue) confirmed several other repositories with the same bug —
stations.repository.ts,community-stations.repository.tsprice/rating fields, and thefuel-logs.repository.tsenhanced methods that return raw rows without a mapper. Those are not addressed here because:.toFixed()).Follow-up issues filed: see issue tracker (linked in #241 comment).
Test plan
npm run type-check(backend): clean.npm run type-check(frontend): clean.npm run lint(backend, scoped to edited files): 0 errors; 14 baselineanywarnings unchanged.npm run lint(frontend, scoped to edited files): 0 errors; 3 baselineanywarnings unchanged.npx jest --testPathPattern='(maintenance|ownership-costs)'(backend): 16 passed; 1 pre-existing compile error inmaintenance.integration.test.tsunchanged frommain.npx jest --testPathPattern='(maintenance|vehicles|ownership-costs)'(frontend): 23 passed.https://staging.motovaultpro.com/garage/vehicles/01caf9e8-2b9a-4f24-959f-67a3e6bd91b1and confirm:$1,234.50).Acceptance criteria (from #241)
mapMaintenanceRecordcoerces allnumeric/decimalfields tonumber.OwnershipCost.amountcoerced.Number()coercions in maintenance frontend components removed.main.