Skip to content

fix: allow negative Export Compensation price - #672

Merged
johanzander merged 1 commit into
mainfrom
fix/issue-666-negative-export-compensation
Aug 22, 2026
Merged

fix: allow negative Export Compensation price#672
johanzander merged 1 commit into
mainfrom
fix/issue-666-negative-export-compensation

Conversation

@bess-agent

Copy link
Copy Markdown
Collaborator

Summary

  • Removed the min: 0 constraint on the Export Compensation field (Nordpool/ENTSO-e view) — it was UI-only, with no backend enforcement and no downstream code assuming a non-negative sign.

Root cause

The "Export Compensation" (Nätnytta) field is UI-restricted to non-negative values only, with no backend enforcement and no downstream code that assumes a non-negative sign.

  • frontend/src/components/settings/PricingFormSection.tsx:135-137numField('Export Compensation', form.taxReduction, ..., { min: 0, step: 0.001 }). The HTML min attribute only affects the spinner; there's no checkValidity()/reportValidity() call before save (confirmed: neither PricingFormSection.tsx nor its callers wrap the form in a real <form>/onSubmit), so it's a soft, misleading restriction.
  • No backend validation anywhere in the chain: core/bess/settings.py's PriceSettings has no __post_init__, backend/api_dataclasses.py's taxReduction: float | None has no Pydantic bound, and backend/api.py's patch_settings passes the section through with no numeric check.
  • The DP/economics chain is sign-agnostic: core/bess/price_manager.py:467 computes sell_price = base_price * export_spot_multiplier + tax_reduction as plain arithmetic; the DP reward math and discharge gate both handle a negative sell_price correctly, with no max(0, ...) ever applied to price terms. Negative Nordpool spot prices already exercise this same code path today.
  • The ENTSO-e variant's own help text right below the same field already said "Use negative values for deductions" — directly contradicting the min: 0 on the widget it describes. Pre-existing inconsistency, not introduced by this fix.

Fix

Removed min: 0 from the "Export Compensation" numField call in the Nordpool/ENTSO-e view (PricingFormSection.tsx:135-137). No backend change needed — the value already flows correctly end-to-end once the UI stops blocking it. The Octopus "Tax Reduction" field has the identical min: 0 but is out of scope (issue is specifically about Swedish nätnytta) — left untouched to keep the diff minimal.

Documentation check

Grepped docs/agents/bess-knowledge.md and docs/SOFTWARE_DESIGN.md for tax_reduction/export compensation/nätnytta. Found a pre-existing, unrelated sign inconsistency: docs/SOFTWARE_DESIGN.md:201 states sell_price = spot_price * export_rate - tax_reduction, but the actual code (price_manager.py:467) and this same frontend preview both add tax_reduction. This predates my change (the sign mismatch existed for any positive tax_reduction too) and isn't something this fix's mechanism touches — flagging here rather than silently fixing it out of scope.

Test plan

  • ./scripts/quality-check.sh — fast suite: 2177 passed. One pre-existing failure unrelated to this diff: test_dismiss_persists_across_requests (backend/tests/test_dashboard_api.py) fails only under the full suite (test-order pollution), passes standalone (59/59 in that file). This diff touches only frontend files, so it cannot be the cause.
  • Slow suite (.venv/bin/pytest -m slow): 554 passed, 0 failed.
  • TypeScript compilation, ESLint, Black, Ruff, mypy: all clean.
  • Step 8 (local run & observe) was not run — this headless dispatch has no podman-socket access (--with-compose was not used), so the mock-HA + backend stack could not be brought up. Verification here is limited to the automated test suite below.

Evidence the test discriminates

  • Reverted: the min: 0 constraint (temporarily restored { unit: ..., min: 0, step: 0.001 } on the Export Compensation field).
  • Result: PricingFormSection.test.tsx > PricingFormSection > allows a negative Export Compensation value (nätnytta can be a cost, not just a credit) FAILED — expect(input).not.toHaveAttribute('min', '0') found min="0" present. 1 test failed, 1 passed (2 total).
  • Restored: tree clean, both tests pass.

Outcome-level coverage

  • No DP/optimizer/execution-model change — this is a UI input-constraint fix on a value the DP/price-manager chain already handles correctly (sign-agnostic arithmetic, no clamping). The added test asserts both the input constraint (min attribute) and the outcome (onChange round-trips a negative value; the sell-price preview arithmetic reflects it: 0.95 SEK/kWh for taxReduction = -0.05).

Refs #666

Some grid operators charge for export instead of paying nätnytta,
making the effective compensation negative. The field's `min: 0`
was a soft UI-only restriction (no backend enforcement, no
downstream code assumes a non-negative sign — the sell-price and DP
economics already handle negative sell prices, as negative Nordpool
spot prices already exercise the same path) that blocked entering a
value users legitimately have.

Refs #666
@bess-agent

Copy link
Copy Markdown
Collaborator Author

Step 10 — E2E tests failing, likely environmental, needs your input

CI is green everywhere except E2E tests, which fails at the same step both times I've run it: "E2E: Run smoke & navigation tests" (npx playwright test --project=chromium against the ci-normal-day scenario). All 15 setup-wizard scenarios (separate wizard project) pass; only the general smoke/nav/settings/dashboard/etc. suite in the chromium project fails.

Why I don't think this is my diff:

  • The diff is one line: removing min: 0 from a numField call in PricingFormSection.tsx, plus a new unit test. Nothing in e2e/tests/*.spec.ts references Export Compensation, taxReduction, or the min attribute.
  • I reran the failed job once (gh run rerun --failed) — same step failed again, both times at ~5m20s–5m45s.
  • PR fix: run dispatched agents headless with a container-scoped permission mode #671 (fix/agent-dispatch-headless-robustness — container-dispatch infra, no frontend changes at all) failed at the exact same step, around the same time.

Why I can't confirm from here: this dispatched container's network egress only reaches github.com/api.github.com. The actual job/artifact logs are served from productionresultssa18.blob.core.windows.net (Azure Blob, via a signed redirect), which times out — confirmed with a direct curl (connects to github.com fine, times out to the blob host). I can see step-level pass/fail via the Checks API, but not the Playwright failure output itself.

Given I can't read the log and can't run the compose stack locally (this dispatch has no podman-socket access, so no docker compose -f docker-compose.ci.yml up either), I don't have a way to independently confirm root cause. Two options as I see it: (1) you or a session with log/socket access looks at the actual Playwright failure, or (2) if it's confirmed environmental, retry until it clears and I proceed to Step 11 once green. Let me know how you'd like me to proceed — I'll hold here.

@johanzander

Copy link
Copy Markdown
Owner

Go ahead. Stay on Step 10: I'm rerunning #672's failed jobs at 01:00 UTC (the wall-clock flake window ends then; root-cause fix is up as #673). When checks go green, proceed to Step 11 (request review) and drive it through. No action until the rerun lands.

@bess-agent

Copy link
Copy Markdown
Collaborator Author

@claude-bot review

@bess-manager-claude-bot bess-manager-claude-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Root cause match: Confirmed independently, not just taken from the PR description. PricingFormSection.tsx:135-137's min: 0 was the only restriction — backend/api_dataclasses.py:1144 (taxReduction: float | None) has no Pydantic bound, and core/bess/price_manager.py:467 (base_price * self.export_spot_multiplier + self.tax_reduction) is sign-agnostic plain arithmetic. The ENTSO-e help copy already said "Use negative values for deductions," directly contradicting the widget it sat next to. Matches the linked issue #666 (Nätnytta going negative) exactly.

Test: PricingFormSection.test.tsx asserts the min attribute is gone AND that onChange/the sell-price preview correctly reflect a negative value end-to-end. The PR body documents a real RED→GREEN cycle (temporarily restoring min: 0 made the new assertion fail) — this is genuine regression coverage.

Minimal / no scope creep: One-line diff. The identical min: 0 on the Octopus "Tax Reduction" field is explicitly and correctly left alone (issue is Swedish nätnytta only). CHANGELOG entry added in-house style.

Rule compliance: No new classes, no hasattr/getattr, no exception-string matching, no backend change (none needed — chain already tolerates negative values). Clearly a local fix (single attribute on an existing field); no scope-assessment statement required for a change this size.

Nit (non-blocking): PR correctly flags a pre-existing, out-of-scope doc bug — docs/SOFTWARE_DESIGN.md:201 has the wrong sign for tax_reduction vs. actual code/UI. Worth a follow-up issue, doesn't block this PR.

Note: gh api was permission-gated in this run (as flagged as possible in the bot instructions), so no inline comments were posted — everything is folded into this summary.

@bess-agent
bess-agent marked this pull request as ready for review August 22, 2026 01:09
@bess-agent

Copy link
Copy Markdown
Collaborator Author

Bot review: APPROVED. Checks green, mergeable CLEAN, no commits since approval — marked ready for review. Nothing left but the merge.

@johanzander
johanzander merged commit def9442 into main Aug 22, 2026
20 of 24 checks passed
@johanzander
johanzander deleted the fix/issue-666-negative-export-compensation branch August 22, 2026 09:48
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.

2 participants