Skip to content

feat: FX-normalized multi-currency earnings summary (Slice 2) - #30

Closed
GeiserX wants to merge 2 commits into
slice0/reconcile-wipfrom
slice2/exchange-earnings
Closed

GeiserX wants to merge 2 commits into
slice0/reconcile-wipfrom
slice2/exchange-earnings

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Slice 2 — FX-normalized multi-currency earnings

Stacked on Slice 0 (#29). Fixes the headline earnings bug: the dashboard summed mixed currencies as one unit (USD + crypto tokens + points) and showed hardcoded $0.00 for Today / This-Month.

What this adds

  • internal/exchange — CoinGecko (crypto→USD) + Frankfurter (USD→fiat), 15-min stale-graceful cache; ToUSD / FromUSD / ToDisplay / Convertible. Non-money points (e.g. GRASS) are never summed.
  • store.ListDailyBalances — latest successful balance per (platform, day).
  • app.go computeEarningsSummary — Total / Today / This-Month (+ change %), a 30-day daily-accrual chart series, a per-service breakdown with payout progress (from the catalog cashout threshold, only when currencies are comparable), and a separate points list. Wired into AppState + a GetEarningsSummary binding; a background goroutine refreshes rates every 15 min.
  • Frontend — real Total / Today / This-Month cards, FX-converted balances (native amount as sub-text), payout progress bars, a points section, and a "rates stale" badge.

Verified

  • go build / go vet / go test -race ./... green (incl. new internal/exchange, ListDailyBalances, and computeEarningsSummary tests).
  • Frontend tsc --noEmit clean.
  • Full wails build (darwin/arm64) succeeds end-to-end on the Mac mini — bindings regenerate, frontend bundles, .app builds.

Base is slice0/reconcile-wip; it auto-retargets to main when #29 merges.

Replace the naive same-unit balance sum with a currency-aware earnings summary
so the dashboard shows a correct total and real today / this-month figures
instead of hardcoded $0.00 placeholders.

- internal/exchange: CoinGecko (crypto->USD) + Frankfurter (USD->fiat) with a
  15-min stale-graceful cache; ToUSD / FromUSD / ToDisplay / Convertible. Non-
  money points (GRASS) are never summed — shown separately.
- store.ListDailyBalances: latest successful balance per (platform, day).
- app.go: computeEarningsSummary — Total / Today / Month (+ change%), a 30-day
  daily-accrual chart series, per-service breakdown with payout progress (from
  the catalog cashout threshold, only when currencies are comparable), and a
  separate points list. Wired into AppState + a GetEarningsSummary binding; a
  background goroutine refreshes rates every CacheTTL.
- frontend: real Total / Today / This-Month cards, FX-converted balances with
  the native amount as sub-text, payout progress bars, a points section, and a
  "rates stale" badge.

Tests: internal/exchange (httptest), store.ListDailyBalances, and
computeEarningsSummary (app_test.go). go build/vet + go test -race ./... green;
frontend tsc --noEmit clean.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62d7c819-4513-4197-93d5-55e914f722cc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slice2/exchange-earnings

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 Jul 4, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

Address the review findings on the FX-normalized earnings summary:

- Per-platform accrual deltas (was whole-portfolio subtraction): a platform's
  first-ever observation, or a baseline outside the fetch window, now contributes
  0 to Today/Month/Daily instead of dumping its whole lifetime balance into the
  hero tiles and chart.
- Widen the daily-balance window to reach the prior-month baseline so MonthChange
  is correct (was wrong ~2/3 of each month with a fixed 40-day window).
- Classify points by intent (wire exchange.PointsCurrencies / IsPoints): a
  convertible currency that is temporarily unpriced (rate outage) is excluded from
  the total and flags rates-stale, rather than being mislabeled a non-money reward
  point and dropped.
- store.ListDailyBalances: deterministic intra-day tie-break on MAX(id) — the
  RFC3339Nano created_at is lexicographically unsafe for same-second rows.
- formatBalance: sanitize the currency code (closes two pre-existing unescaped
  innerHTML sinks); show the native balance when a display rate is missing.
- Clamp payout Percent to [0,100]; require MinAmount>0 for Eligible.

Tests: +6 (first-observation, carry-forward gap, month-window value,
points-during-outage, id tie-break, IsPoints). go test -race ./... green; tsc clean.
@GeiserX

GeiserX commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

Review triage (Slice 2 earnings)

Two in-loop review passes (earnings-math + frontend/tests) — no XSS in the new code; the findings were numeric-aggregation correctness issues, all fixed in dbc4b91.

Fixed

  • MonthChange wrong ~2/3 of each month (HIGH): the 40-day fetch window couldn't reach the prior-month baseline. The window is now sized to the actual lookback, and the accrual math is per-platform so an unreachable baseline degrades to "contributes 0" instead of a garbage %.
  • First-ever observation inflated Today/Month/chart (MEDIUM): a platform's first collection dumped its whole lifetime balance as that day's earning. Per-platform deltas now contribute 0 without an established prior baseline.
  • Convertible currency mislabeled as "points" during a rate outage (MEDIUM): PointsCurrencies was defined but never wired. Points are now classified by intent; a temporarily-unpriced currency (e.g. MYST mid-outage) is excluded from the total and flags rates-stale, not dropped into the points list.
  • Same-second row ordering (LOW): ListDailyBalances tie-breaks on MAX(id) (RFC3339Nano created_at is lexicographically unsafe).
  • formatBalance sanitize (LOW/med): closes two pre-existing unescaped innerHTML sinks; native fallback when a display rate is missing; payout percent clamped, eligibility requires a positive threshold.

+6 regression tests (one fails under the old code, proving the fix). go test -race ./... green; frontend tsc clean; full wails build verified on the base commit.

@GeiserX
GeiserX deleted the branch slice0/reconcile-wip July 5, 2026 00:38
@GeiserX GeiserX closed this Jul 5, 2026
@GeiserX
GeiserX deleted the slice2/exchange-earnings branch July 5, 2026 00:56
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