fix: stations and community-stations repository mappers return DECIMAL prices as strings #243

Closed
opened 2026-05-16 02:07:13 +00:00 by egullickson · 1 comment
Owner

Summary

Per the audit performed for #241, the stations features have the same numeric-as-string bug. node-postgres returns numeric/decimal columns as strings, but the TypeScript interfaces declare them as number. The mappers pass values through raw.

Affected

backend/src/features/stations/data/stations.repository.ts

Cache row mapper returns these fields without coercion:

  • priceRegularDECIMAL(10,2)
  • pricePremiumDECIMAL(10,2)
  • priceDieselDECIMAL(10,2)
  • ratingDECIMAL(2,1)

backend/src/features/stations/data/community-stations.repository.ts

  • mapRow.price93DECIMAL(5,3)

Investigation Required (before fixing)

Unlike maintenance and ownership-costs, the stations price fields may be consumed in places that already work around the string-as-number behavior (e.g., parsing back to number for sorting, displaying as-is in formatted strings). Before fixing the mapper:

  • Find every consumer of priceRegular, pricePremium, priceDiesel, rating, and price93 — backend and frontend.
  • For each consumer, decide whether coercing to number changes behavior (e.g., a <sort by price> UI that does Number(a.priceRegular) - Number(b.priceRegular) will continue to work; a UI that does priceRegular.includes(".") would break).

Suggested Fix (mapper layer)

Match the pattern used in vehicles.repository.ts and the fix applied in PR #242:

priceRegular: row.price_regular != null ? Number(row.price_regular) : undefined,
priceDiesel: row.price_diesel != null ? Number(row.price_diesel) : undefined,
priceRegular: row.price_premium != null ? Number(row.price_premium) : undefined,
rating: row.rating != null ? Number(row.rating) : undefined,
// community-stations:
price93: row.price_93 != null ? Number(row.price_93) : undefined,

Acceptance Criteria

  • Audit downstream consumers of every affected field (backend services, frontend components, tests).
  • Decide which fields to coerce based on consumer analysis.
  • Update mappers accordingly.
  • Remove any defensive Number() calls in consumers that become unnecessary.
  • Mobile + desktop verification of any UI surfaces that display these prices.
  • Linting, type-check, and tests pass.
  • #241 / PR #242 — fixed the same pattern in maintenance and ownership-costs.
## Summary Per the audit performed for #241, the stations features have the same numeric-as-string bug. node-postgres returns `numeric`/`decimal` columns as strings, but the TypeScript interfaces declare them as `number`. The mappers pass values through raw. ## Affected ### `backend/src/features/stations/data/stations.repository.ts` Cache row mapper returns these fields without coercion: - `priceRegular` — `DECIMAL(10,2)` - `pricePremium` — `DECIMAL(10,2)` - `priceDiesel` — `DECIMAL(10,2)` - `rating` — `DECIMAL(2,1)` ### `backend/src/features/stations/data/community-stations.repository.ts` - `mapRow.price93` — `DECIMAL(5,3)` ## Investigation Required (before fixing) Unlike maintenance and ownership-costs, the stations price fields may be consumed in places that already work around the string-as-number behavior (e.g., parsing back to number for sorting, displaying as-is in formatted strings). Before fixing the mapper: - Find every consumer of `priceRegular`, `pricePremium`, `priceDiesel`, `rating`, and `price93` — backend and frontend. - For each consumer, decide whether coercing to `number` changes behavior (e.g., a `<sort by price>` UI that does `Number(a.priceRegular) - Number(b.priceRegular)` will continue to work; a UI that does `priceRegular.includes(".")` would break). ## Suggested Fix (mapper layer) Match the pattern used in `vehicles.repository.ts` and the fix applied in PR #242: ```typescript priceRegular: row.price_regular != null ? Number(row.price_regular) : undefined, priceDiesel: row.price_diesel != null ? Number(row.price_diesel) : undefined, priceRegular: row.price_premium != null ? Number(row.price_premium) : undefined, rating: row.rating != null ? Number(row.rating) : undefined, // community-stations: price93: row.price_93 != null ? Number(row.price_93) : undefined, ``` ## Acceptance Criteria - [ ] Audit downstream consumers of every affected field (backend services, frontend components, tests). - [ ] Decide which fields to coerce based on consumer analysis. - [ ] Update mappers accordingly. - [ ] Remove any defensive `Number()` calls in consumers that become unnecessary. - [ ] Mobile + desktop verification of any UI surfaces that display these prices. - [ ] Linting, type-check, and tests pass. ## Related - #241 / PR #242 — fixed the same pattern in maintenance and ownership-costs.
egullickson added the
status
backlog
type
bug
labels 2026-05-16 02:07:29 +00:00
egullickson added
status
in-progress
and removed
status
backlog
labels 2026-05-16 02:59:14 +00:00
Author
Owner

Closing as not-a-bug — original audit was wrong

Investigated this before fixing and discovered the mappers already coerce these fields. The audit that originated this issue (the one performed for #241) claimed the stations mappers "return raw rows" but it was inferring from the audit summary table without actually reading the files.

Actual state of the code

backend/src/features/stations/data/stations.repository.ts:157-172mapCacheRow:

priceRegular: row.price_regular ? parseFloat(row.price_regular) : undefined,
pricePremium: row.price_premium ? parseFloat(row.price_premium) : undefined,
priceDiesel: row.price_diesel ? parseFloat(row.price_diesel) : undefined,
rating: row.rating ? parseFloat(row.rating) : undefined,

backend/src/features/stations/data/community-stations.repository.ts:308-333mapRow:

price93: row.price_93 ? parseFloat(row.price_93) : undefined,

A grep across the stations feature confirms there is no path that bypasses these mappers — every consumer accesses the camelCase mapped object, the domain types declare number, and the API validator (api/community-stations.validation.ts:19) uses z.number(). End-to-end the type contract is already honest.

Minor code-quality detail (not fixing here)

The truthiness check pattern row.field ? parseFloat(...) : undefined would drop a legitimate 0 value (e.g., a station rated 0.0 by Google Maps, or — implausibly — free fuel). Same pattern exists in vehicles.repository.ts already on main. If you want to tighten this to != null everywhere, that's a separate code-quality issue worth filing on its own. It is not what #243 was reporting, so closing this as not-a-bug rather than expanding scope.

Lesson for future audits

The audit table that fed #241 marked stations as "needs fix: YES" based on phrases like "(mapCacheRow) not shown but inferred to return raw rows" — i.e. it never opened the file. Future audits should not flag a file as broken without reading it. The fuel-logs audit done for #244 caught its own errors precisely because the agent did read the file.

Closing.

## Closing as not-a-bug — original audit was wrong Investigated this before fixing and discovered the mappers **already coerce these fields**. The audit that originated this issue (the one performed for #241) claimed the stations mappers "return raw rows" but it was inferring from the audit summary table without actually reading the files. ### Actual state of the code **`backend/src/features/stations/data/stations.repository.ts:157-172`** — `mapCacheRow`: ```typescript priceRegular: row.price_regular ? parseFloat(row.price_regular) : undefined, pricePremium: row.price_premium ? parseFloat(row.price_premium) : undefined, priceDiesel: row.price_diesel ? parseFloat(row.price_diesel) : undefined, rating: row.rating ? parseFloat(row.rating) : undefined, ``` **`backend/src/features/stations/data/community-stations.repository.ts:308-333`** — `mapRow`: ```typescript price93: row.price_93 ? parseFloat(row.price_93) : undefined, ``` A grep across the stations feature confirms there is no path that bypasses these mappers — every consumer accesses the camelCase mapped object, the domain types declare `number`, and the API validator (`api/community-stations.validation.ts:19`) uses `z.number()`. End-to-end the type contract is already honest. ### Minor code-quality detail (not fixing here) The truthiness check pattern `row.field ? parseFloat(...) : undefined` would drop a legitimate `0` value (e.g., a station rated 0.0 by Google Maps, or — implausibly — free fuel). Same pattern exists in `vehicles.repository.ts` already on `main`. If you want to tighten this to `!= null` everywhere, that's a separate code-quality issue worth filing on its own. **It is not what #243 was reporting, so closing this as not-a-bug rather than expanding scope.** ### Lesson for future audits The audit table that fed #241 marked stations as "needs fix: YES" based on phrases like "(mapCacheRow) not shown but inferred to return raw rows" — i.e. it never opened the file. Future audits should not flag a file as broken without reading it. The fuel-logs audit done for #244 caught its own errors precisely because the agent did read the file. Closing.
egullickson added
status
done
and removed
status
in-progress
labels 2026-05-16 03:01:55 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#243