Skip to content

fix: net earnings subtracted a EUR cost from a USD gross (beads batch 13) - #196

Merged
GeiserX merged 2 commits into
mainfrom
fix/beads-batch-13
Aug 4, 2026
Merged

fix: net earnings subtracted a EUR cost from a USD gross (beads batch 13)#196
GeiserX merged 2 commits into
mainfrom
fix/beads-batch-13

Conversation

@GeiserX

@GeiserX GeiserX commented Aug 4, 2026

Copy link
Copy Markdown
Owner

dlrgross is USD by contract; cost comes from a tariff in power_currency. Subtracting one from the other produced a number in neither, labelled with the tariff currency.

At ~1.08 USD/EUR that's an 8% error, in the direction that flatters the result, on the single figure that decides whether a machine is worth keeping powered on.

The tariff is converted into USD, not the gross into the tariff currency. My first attempt did the latter and it was wrong: every other money figure in this API is canonical USD and the frontend has a display-currency layer, so converting the gross would have made this endpoint the exception. Converting the price keeps the convention and needs no new rate direction.

With no rate available it reports cost_known: false and says why, rather than inventing a net. That mirrors what the module already does with no tariff at all — "a zero cost would render gross as net and quietly overstate earnings." Passing "USD" as a label while the price stayed EUR-denominated (my second attempt) only moves the mixing.

conftest now seeds the fiat rates a running system always has. exchange_rates.refresh() populates them at startup and every 15 minutes, but the test process never calls it, so _fiat_rates was empty everywhere. Ten tests configure a EUR tariff and expect a cost; with no rate the endpoint correctly refuses, so they failed for reasons unrelated to what they test.

Two existing assertions changed, both encoding the defect — test_power expected currency == "EUR" on a figure whose gross was USD.

Verification: 2533 tests, 95.12% coverage, both ruff gates, JS parses. Negative control: restoring the mixing fails four tests.

Summary by CodeRabbit

  • New Features
    • Net earnings now support electricity tariffs priced in non-USD currencies by converting them to USD.
    • Responses identify the tariff currency and explain when conversion rates are unavailable.
  • Bug Fixes
    • Prevented misleading net earnings when currency conversion data is missing.
    • Corrected exchange-rate direction and preserved gross earnings in USD.
    • Improved handling of native-currency earnings and pricing scenarios.
  • Tests
    • Added coverage for currency transitions, tariff conversion, missing rates, and related earnings calculations.

GeiserX added 2 commits August 4, 2026 13:47
CashPilot-dlr. gross comes from get_earned_by_platform, which is USD by
contract. cost is computed from a tariff the user entered in
power_currency. Subtracting one from the other produced a number in
neither, labelled with the tariff currency.

At roughly 1.08 USD/EUR that is an ~8% error, in the direction that
flatters the result, on the single figure that decides whether a machine
is worth keeping powered on.

Gross is now converted into the tariff currency first, so the
subtraction is like-for-like. Measured on a seeded request: 100 USD
gross with a 0.92 rate reports 92.00 EUR against 3.97 EUR of
electricity.

exchange_rates gains from_usd, deliberately next to to_usd because the
direction is the easy thing to get wrong: _fiat_rates holds USD->X
rates, so from_usd MULTIPLIES where to_usd divides. Inverting it would
report 109 EUR for 100 USD and flatter the number further — worse than
the bug. A round-trip assertion pins it.

Fiat only. A USD figure has no meaningful expression in a provider's
token, and inventing one would put a crypto amount where a currency
belongs.

With no rate available the response stays in USD and says so via
fx_unavailable and tariff_currency, rather than quietly mixing the two —
the same rule the module already follows when no tariff is set at all.

Negative control: removing the conversion fails four tests.
CashPilot-dlr. gross comes from get_earned_by_platform, which is USD by
contract; cost is computed from a tariff the user entered in
power_currency. Subtracting one from the other produced a number in
neither, labelled with the tariff currency — at ~1.08 USD/EUR an 8%
error, in the direction that flatters the result, on the figure that
decides whether a machine is worth keeping powered on.

The TARIFF is converted into USD, not the gross into the tariff
currency. My first attempt did the latter and it was the wrong call:
every other money figure in this API is canonical USD and the frontend
has a display-currency layer, so converting the gross would have made
this one endpoint the exception. Converting the price keeps the
convention and needs no new rate direction.

With no rate the endpoint reports cost_known: false and says why, rather
than producing a net from two currencies. That mirrors what the module
already does with no tariff at all: "a zero cost would render gross as
net and quietly overstate earnings." Passing "USD" as a label while the
price stayed EUR-denominated — which I tried first — only moves the
mixing rather than removing it.

conftest now seeds the fiat rates a RUNNING system always has.
exchange_rates.refresh() populates them at startup and every 15 minutes,
but the test process never calls it, so _fiat_rates was empty
everywhere. Ten tests configure a EUR tariff and expect a cost; with no
rate the endpoint correctly refuses, and they failed for a reason
unrelated to what they test. Seeded rather than mocked per test, because
"a rate exists" is the normal state.

Two existing assertions changed, both of which encoded the defect:
test_power expected currency EUR on a figure whose gross was USD.

Negative control: restoring the mixing fails four tests.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b99d0ca-0313-4414-ae66-238daa13cc19

📥 Commits

Reviewing files that changed from the base of the PR and between f0f5a0a and 0a63074.

📒 Files selected for processing (5)
  • app/exchange_rates.py
  • app/main.py
  • tests/conftest.py
  • tests/test_currency_correctness.py
  • tests/test_power.py

📝 Walkthrough

Walkthrough

Changes

Currency normalization

Layer / File(s) Summary
USD conversion helper and test isolation
app/exchange_rates.py, tests/conftest.py, tests/test_currency_correctness.py
Adds from_usd for cached fiat conversion. Tests seed and restore exchange-rate state.
Net earnings tariff conversion and validation
app/main.py, tests/test_currency_correctness.py, tests/test_power.py
Converts tariffs to USD before subtraction. Unavailable rates return cost metadata instead of mixed-currency totals. Tests cover conversion direction and endpoint output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: converting EUR costs before subtracting them from USD gross earnings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/beads-batch-13

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.07%. Comparing base (f0f5a0a) to head (0a63074).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
app/exchange_rates.py 71.42% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #196      +/-   ##
==========================================
- Coverage   95.09%   95.07%   -0.02%     
==========================================
  Files          46       46              
  Lines        5889     5908      +19     
==========================================
+ Hits         5600     5617      +17     
- Misses        289      291       +2     
Files with missing lines Coverage Δ
app/main.py 97.34% <100.00%> (+0.02%) ⬆️
app/exchange_rates.py 91.78% <71.42%> (-2.16%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@GeiserX

GeiserX commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
GeiserX merged commit 52b6ad6 into main Aug 4, 2026
8 checks passed
@GeiserX
GeiserX deleted the fix/beads-batch-13 branch August 4, 2026 12:54
GeiserX added a commit that referenced this pull request Aug 4, 2026
Every gross in /api/fleet/economics comes from get_earned_by_platform, which
is USD by contract. The electricity price came straight from the tariff config,
in whatever currency the user set, and machine_economics does net = gross-cost.

Live that rendered gross 5.00 (USD) minus cost 14.24 (EUR, 65 W at EUR
0.30/kWh) = net -9.24, verdict "losing money", with "turning it off would save
that" — advice to switch off hardware, wrong by the whole FX spread. The true
net was about -$11.35, not -9.24 of any currency. With a weaker tariff currency
the error is not 15% but orders of magnitude.

The payload also carried no currency field at all, and fleet.html rendered
Number(v).toFixed(2), so the user saw bare numbers they could not attribute to
a unit — while those numbers silently mixed two.

Fixed the way /api/earnings/net was in #196: convert the TARIFF to USD, so the
endpoint stays canonical USD like the rest of the API and the frontend's
display-currency layer renders it in whatever the viewer reads in. When no rate
is available the cost is None, not zero — a zero cost would render gross as net
and overstate earnings, which is the mistake this module already guards against
when no tariff is set at all.

formatCurrency is now exported on CP so fleet.html can reach the one function
that knows the viewer's display currency. null still renders as an em dash, so
"not reporting" stays distinct from "earns nothing".

Negative control, each part separately: not converting the tariff fails 2 tests;
treating a missing rate as usable fails 1; dropping the currency label fails 1;
restoring the bare-number renderer fails 1. Nothing overlaps.

Closes CashPilot-dlr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant