feat(prices): every European bidding zone, billed in its own currency - #748
feat(prices): every European bidding zone, billed in its own currency#748frahlg wants to merge 2 commits into
Conversation
The zone picker offered twelve Nordic codes typed into two <select> elements and the Go side knew twelve EIC codes, so a household in Belgium could not choose its own zone — even though the Sourceful price API has served all 46 ENTSO-E areas all along. Both lists now come from one table, generated from that API's /areas endpoint and served to the UI at GET /api/prices/zones. The picker asks for a country first and a zone second: everyone knows they live in Italy, nobody knows their area code is IT-CENTRE-NORTH. Currency follows the zone. The price API converts to EUR and SEK and quietly answers anything else with EUR, so it is only asked for those two; the rest is converted here from EUR with the ECB rates the currency service already caches. A missing rate now fails the fetch instead of falling back to 11.5 SEK/EUR — a price wrong by an exchange rate is one the planner spends money on. Two faults surfaced on the way. The direct ENTSO-E provider assumed every day-ahead document is priced in EUR, but Poland and Hungary among others publish in their own currency, so it reads currency_Unit.name and converts from that. Its German EIC was the country code rather than the DE-LU bidding zone, and NO5 was missing outright. Stored prices are minor units per kWh with no currency attached, so a currency change clears the price cache rather than letting cost history add öre to cent. Every price label in the UI now follows the configured currency, including the major unit where the minor one is out of circulation — 4.30 Kč/kWh, not 430 haléř. Existing installs are untouched: no zone means SE3, no currency means SEK, and a Swedish install still asks the API for SEK exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The assertion matched the whole script tag including `?v=dashboard1`, so bumping the token — which every change to plan.js has to do, or a returning browser keeps the old file — failed a test about module loading. Match the token loosely and keep the point: the page loads plan.js as a module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@erikarenhill — over to you, and yours to merge when you're happy with it. This started as a user question from Belgium: can they pick their own price Most of the diff is
Verified against the live API rather than fixtures: zone Existing installs are untouched: no zone means SE3, no currency means SEK. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90de0830a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case "elprisetjustnu": | ||
| p = NewElpriser() |
There was a problem hiding this comment.
Keep elprisetjustnu prices in SEK
When price.currency is anything other than SEK—either selected manually or left as EUR after switching a foreign zone to this Sweden-only provider—ElpriserProvider still returns its SEK_per_kWh field unchanged, while the service exposes and stores the configured currency. The resulting SEK values are therefore labelled as EUR/NOK/etc. and can misprice planner decisions by the exchange-rate factor; force SEK for this provider or convert its output.
Useful? React with 👍 / 👎.
| prev, ok := s.Store.LoadConfig(priceCurrencyKey) | ||
| if ok && prev == s.Currency { | ||
| return | ||
| } | ||
| if ok && prev != "" { |
There was a problem hiding this comment.
Invalidate legacy Sourceful rows when adding the currency marker
On the first upgrade, prices/currency is absent, so this condition preserves every existing row and immediately records the new currency. Before this commit Sourceful always stored SEK regardless of zone, but an existing NO/DK/etc. configuration with no explicit currency now infers its local currency; historical SEK rows then remain permanently mixed with newly fetched local-currency rows and corrupt savings and price-forecast calculations. The no-marker migration needs to infer the legacy provider's actual stored currency and clear when it differs.
Useful? React with 👍 / 👎.
| currency := strings.ToUpper(strings.TrimSpace(cfg.Currency)) | ||
| if currency == "" { | ||
| currency = ZoneCurrency(zone) | ||
| } |
There was a problem hiding this comment.
Scale cold-start forecasts to the selected currency
For a newly supported non-SEK zone, storing prices in the zone's local minor units feeds them into the unchanged priceforecast service, whose cold-start model is a Nordic 50–80 öre prior and whose persisted models carry no currency. The MPC extends day-ahead prices with that model, so a fresh Hungarian install, for example, receives roughly 40 fillér/kWh rather than tens of HUF/kWh beyond the published horizon, which can drive incorrect charging and discharging until enough history is refitted. Reset and currency-scale the forecast model when deriving a different currency.
Useful? React with 👍 / 👎.
| if convert { | ||
| native, ok := s.FX.Convert(perKWh, apiCur, want) | ||
| if !ok { | ||
| return nil, fmt.Errorf("sourceful: no %s→%s rate yet", apiCur, want) |
There was a problem hiding this comment.
Retry once initial ECB rates become available
On a fresh NOK/DKK/PLN/etc. installation there are no cached ECB rates, and currency.Service.Start fetches them asynchronously while the price service starts its initial fetch immediately. If Sourceful responds before the ECB request, this conversion returns an error for both requested days; prices.Service.loop then waits for its one-hour ticker even if the FX rates arrive moments later, leaving the dashboard and planner without prices for up to an hour. Coordinate the initial fetch with FX readiness or schedule a prompt retry for this transient missing-rate case.
Useful? React with 👍 / 👎.
A question from a user in Belgium: can they pick their own price zone?
They could not. The zone picker offered twelve Nordic codes typed into two
<select>elements, and the Go side knew twelve EIC codes — even though theSourceful price API has served all 46 ENTSO-E areas all along.
What changed
One zone table.
go/internal/prices/zones.gois generated from the priceAPI's
/areasendpoint and carries area code, country, EIC and localcurrency. It replaces the hand-written
entsoeZoneEICmap and is served tothe UI at
GET /api/prices/zones, so the picker cannot offer a zone thefetchers don't know.
Country first, zone second, in both Settings → Price and the setup
wizard. Everyone knows they live in Italy; nobody knows their area code is
IT-CENTRE-NORTH. elprisetjustnu is held to SE1–SE4, the only zones it canserve.
Currency follows the zone. The price API converts to EUR and SEK and
quietly answers any other currency code with EUR — no error — so it is only
asked for those two, and everything else is converted here from EUR with the
ECB rates
go/internal/currencyalready caches. A missing rate now fails thefetch instead of falling back to a ballpark 11.5 SEK/EUR: a price wrong by an
exchange rate is a price the planner spends money on.
Units follow the currency across every price surface — öre, cent, øre,
grosz, Rappen, and the major unit where the minor one is out of circulation
(4.30 Kč/kWh, not 430 haléř). The savings card and the plan summary printed a
hardcoded
SEKover whatever the numbers really were; they don't now.Two faults found on the way
EUR. Poland and Hungary among others publish in their own currency, so it
reads
currency_Unit.nameand converts from that.10Y1001A1001A83F) ratherthan the DE-LU bidding zone (
10Y1001A1001A82H), and NO5 was missingoutright while the UI offered it.
Cache
Stored prices are minor units per kWh with no currency attached, so changing
the currency clears the price cache — otherwise cost history would add öre to
cent. Prices live in the disposable cache DB and the next fetch refills today
and tomorrow. A price-config change already requires a restart, which is when
the check runs.
Verification
Run against the live API with
zone: BE,currencyunset:192 quarter-hour slots, spot 17.73 cent/kWh → total 32.34 with a 9 cent grid
tariff and 21 % VAT. The dashboard renders
23.4 cent/kWh · BE, the charty-axis ticks in
c, and the savings card inEUR. Switching country inSettings moves the zone list, the currency and every unit label with it;
switching the provider to elprisetjustnu pulls a foreign zone back into
Sweden rather than emptying the select.
Cache-busting
?v=tokens are bumped for every changed script — without thata returning browser keeps the old file, which is exactly how the stale
SEKlabel showed up during testing.Compatibility
Existing installs are untouched: no zone means SE3, no currency means SEK,
and a Swedish install still asks the API for SEK exactly as before.
make verifyclean, 269 web tests pass.🤖 Generated with Claude Code