feat(fuel-prices): add retail gasoline and diesel prices panel (#2150)

* feat(fuel-prices): add retail fuel prices panel and seeder

- Add ListFuelPrices proto RPC with FuelPrice/FuelCountryPrice messages
- Create seed-fuel-prices.mjs seeder with 5 sources: Malaysia (data.gov.my),
  Spain (minetur.gob.es), Mexico (datos.gob.mx), US EIA, EU oil bulletin CSV
- Add list-fuel-prices.ts RPC handler reading from Redis seed cache
- Wire handler into EconomicService handler.ts
- Register fuelPrices in cache-keys.ts, bootstrap.js, health.js, gateway.ts
- Add FuelPricesPanel frontend component with gasoline/diesel/source columns
- Wire panel into panel-layout.ts, App.ts (prime + refresh scheduler)
- Add panel config, command entry with fuel/gas/diesel/petrol keywords
- Add fuelPrices refresh interval (6h) in base.ts
- Add i18n keys in en.json (panels.fuelPrices + components.fuelPrices)

Task: fuel-prices

* fix(fuel-prices): add BR/NZ/UK sources, fix EU non-euro currency, review fixes

- Add fetchBrazil() — ANP CSVs (GASOLINA + DIESEL), Promise.allSettled for
  independent partial results, BRL→USD via FX
- Add fetchNewZealand() — MBIE weekly-table.csv, Board price national avg, NZD→USD
- Add fetchUK_ModeA() — CMA retailer JSON feeds (Asda/BP/JET/MFG/Sainsbury's/
  Morrisons), E10+B7 pence→GBP, max-date observedAt across retailers
- Fix EU non-euro members (BG/CZ/DK/HU/PL/RO/SE) using local currency FX on
  EUR-denominated prices — all EU entries now use currency:'EUR'
- Fix fetchBrazil Promise.all → Promise.allSettled (partial CSV failure no
  longer discards both fuels)
- Fix UK observedAt: keep latest date across retailers (not last-processed)
- Fix WoW anomaly: omit wowPct instead of setting to 0
- Lift parseEUPrice out of inner loop to module scope
- Pre-compute parseBRDate per row to avoid double-conversion
- Update infoTooltip: describe methodology without exposing source URLs
- Add BRL, NZD, GBP to FX symbols list

* fix(fuel-prices): fix 4 live data bugs found in external review

EU CSV: replace hardcoded 2024 URLs (both returning 404) with dynamic
discovery — scrape EC energy page for current CSV link, fall back to
generated YYYY-MM patterns for last 4 months.

NZ: live MBIE header has no Region column (Week,Date,Fuel,Variable,Value,
Unit,Status) — remove regionIdx guard that was forcing return []. Values
are in NZD c/L not NZD/L — divide by 100 before storing.

UK: last_updated is DD/MM/YYYY HH:mm:ss not ISO — parse to YYYY-MM-DD
before lexicographic max-date comparison; previous code stored the
seed-run date instead of the latest retailer timestamp.

Panel: source column fell back to — for diesel-only countries because it
only read gas?.source. Use (gas ?? dsl)?.source so diesel-only rows
display their source correctly.
This commit is contained in:
Elie Habib
2026-03-23 20:26:57 +04:00
committed by GitHub
parent 4f19e36804
commit e08457aadf
21 changed files with 1147 additions and 1 deletions

View File

@@ -391,6 +391,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/economic/v1/list-fuel-prices:
get:
tags:
- EconomicService
summary: ListFuelPrices
description: ListFuelPrices retrieves retail gasoline and diesel prices across 30+ countries.
operationId: ListFuelPrices
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListFuelPricesResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
@@ -1137,3 +1163,70 @@ components:
type: string
description: Human-readable source string.
description: NationalDebtEntry holds debt data for a single country.
ListFuelPricesRequest:
type: object
ListFuelPricesResponse:
type: object
properties:
countries:
type: array
items:
$ref: '#/components/schemas/FuelCountryPrice'
fetchedAt:
type: string
cheapestGasoline:
type: string
cheapestDiesel:
type: string
mostExpensiveGasoline:
type: string
mostExpensiveDiesel:
type: string
wowAvailable:
type: boolean
prevFetchedAt:
type: string
sourceCount:
type: integer
format: int32
countryCount:
type: integer
format: int32
FuelCountryPrice:
type: object
properties:
code:
type: string
name:
type: string
currency:
type: string
flag:
type: string
gasoline:
$ref: '#/components/schemas/FuelPrice'
diesel:
$ref: '#/components/schemas/FuelPrice'
fxRate:
type: number
format: double
FuelPrice:
type: object
properties:
usdPrice:
type: number
format: double
localPrice:
type: number
format: double
grade:
type: string
source:
type: string
available:
type: boolean
wowPct:
type: number
format: double
observedAt:
type: string