Skip to content

fix(recs): disambiguate min-savings dollar (API) vs percent (CLI) filters - #1091

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/min-savings-units
Jun 8, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/min-savings-units

Conversation

@cristim

@cristim cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Renames the API/TypeScript recommendation filter field from minSavings (ambiguous) to minSavingsUsd (dollar floor) and adds a new distinct minSavingsPct (percentage floor, 0-100)
  • Wires both filters in the Go handler (handler_recommendations.go) -- previously min_savings was sent by the frontend but silently dropped server-side (never parsed)
  • MinSavingsUSD is pushed into SQL via the existing monthly_savings column; MinSavingsPct is applied in-process via a new recEffectiveSavingsPct helper that mirrors the frontend effectiveSavingsPct formula
  • Adds parseMinSavingsParam validation helper (non-negative numeric, 0-100 range check for pct)

Rationale (issue #1089)

CLI --min-savings-pct filters by savings percentage (e.g. 30%); the old API min_savings was a dollar amount. A user who knows --min-savings-pct 30 from the CLI would set min_savings=30 in the GUI and silently filter by $30 -- not 30%. The fix makes both concepts explicit and independently accessible.

Files changed

  • internal/config/types.go -- RecommendationFilter: MinSavings renamed to MinSavingsUSD, new MinSavingsPct field
  • internal/config/store_postgres_recommendations.go -- SQL builder uses MinSavingsUSD; ListStoredRecommendations applies MinSavingsPct in-process; new recEffectiveSavingsPct helper
  • internal/api/handler_recommendations.go -- parses both new query params with validation
  • internal/api/validation.go -- new parseMinSavingsParam helper
  • frontend/src/api/types.ts -- RecommendationFilters: minSavings renamed to minSavingsUsd with JSDoc, new minSavingsPct field
  • frontend/src/api/recommendations.ts -- getRecommendations sends min_savings_usd / min_savings_pct query params

Regression tests

  • Go (internal/api/handler_recommendations_test.go): TestGetRecommendations_MinSavingsFilters -- asserts dollar and percentage params map to distinct filter fields
  • Go (internal/api/validation_test.go): TestParseMinSavingsParam -- validates parse edge cases
  • Go (internal/config/store_postgres_savings_filter_test.go): TestRecEffectiveSavingsPct_Config, TestRecommendationFilter_UnitDistinction, TestBuildRecommendationFilter_MinSavingsUSD -- asserts the same numeric value "30" produces opposite filter outcomes as USD vs PCT
  • JS (frontend/src/__tests__/api.test.ts): 3 new tests assert minSavingsUsd -> min_savings_usd and minSavingsPct -> min_savings_pct, never conflated

Verification

  • go test ./internal/api/... ./internal/config/... -- 2095 tests pass
  • npx tsc --noEmit -- no type errors
  • jest --testPathPattern=api.test.ts -- 75 tests pass

Closes #1089

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect labels Jun 7, 2026
@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 56 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4deb944-b1c4-478d-97a5-34b8687c0f4f

📥 Commits

Reviewing files that changed from the base of the PR and between f5632b3 and 94bdc39.

📒 Files selected for processing (10)
  • frontend/src/__tests__/api.test.ts
  • frontend/src/api/recommendations.ts
  • frontend/src/api/types.ts
  • internal/api/handler_recommendations.go
  • internal/api/handler_recommendations_test.go
  • internal/api/validation.go
  • internal/api/validation_test.go
  • internal/config/store_postgres_recommendations.go
  • internal/config/store_postgres_savings_filter_test.go
  • internal/config/types.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/min-savings-units

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

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ 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.

cristim added 2 commits June 7, 2026 14:39
…ters

Closes #1089

The CLI `--min-savings-pct` flag filters by savings percentage while the
API/GUI `min_savings` parameter was intended to filter by absolute dollar
savings -- two differently-scaled concepts with near-identical names.
A user who knows `--min-savings-pct 30` from the CLI would set
`min_savings=30` in the GUI and silently filter by $30, not 30%.

Additionally, the `min_savings` query parameter was never parsed by the
Go handler, so it was silently dropped server-side despite being sent by
the frontend.

Changes:
- Rename `RecommendationFilter.MinSavings` to `MinSavingsUSD` (Go)
- Add `RecommendationFilter.MinSavingsPct` (0-100 percentage floor)
- Parse both `min_savings_usd` and `min_savings_pct` from query params
  in getRecommendations, with input validation (400 on bad values)
- MinSavingsUSD is pushed into SQL (existing monthly_savings column)
- MinSavingsPct is applied in-process via recEffectiveSavingsPct helper
  (mirrors the frontend effectiveSavingsPct formula)
- Rename `RecommendationFilters.minSavings` to `minSavingsUsd` in TS
- Add `minSavingsPct` to RecommendationFilters in TS with clear JSDoc
- Update getRecommendations API call to use new distinct param names
- Regression tests: Go (handler + config + validation) and JS (api.test)
  assert the % and $ paths are distinct and cannot be conflated
…mplexity

getRecommendations had cyclomatic complexity of 12 (limit: 10) after the
min-savings parameter additions. Extract parameter validation and filter
construction into parseRecommendationFilter, dropping getRecommendations
to complexity 5.

No behaviour change; all existing tests pass.
@cristim
cristim force-pushed the fix/min-savings-units branch from 1700339 to 94bdc39 Compare June 7, 2026 21:42

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

@cristim I'll kick off a review of PR #1091 right away.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.069Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.433Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.433Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:28:28.139Z
Learning: In LeanerCloud/CUDly GCP service clients (computeengine, cloudsql, memorystore, cloudstorage), `GetRecommendations` filters out non-ACTIVE recommendations using the typed `recommenderpb.RecommendationStateInfo_ACTIVE` enum (CLAIMED/SUCCEEDED/FAILED/DISMISSED are skipped). State is accessed nil-safely via the proto getter. Fixed in PR `#1047` commit 95a390f (H-1 finding). Regression tests: `TestGetRecommendations_FiltersNonActiveStates` (5-rec mock, only ACTIVE survives) and `TestGetRecommendations_ActiveRecIncluded`. Existing mock tests in all four services had StateInfo added to satisfy the new filter.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.445Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` no longer hardcodes `Term = "1yr"` (fixed in PR `#1047` commit 8f9a787, H-3 audit finding). It propagates `params.Term` with a `"1yr"` default, validates via `termPlan`, and returns nil on unknown term. This is stricter than memorystore/cloudstorage/cloudsql (which default and continue): computeengine returns nil on unknown term because it is on the purchase path. Regression tests: `TestConvertGCPRecommendation_PropagatesParamsTerm`, `TestConvertGCPRecommendation_RejectsUnknownTerm`.
✅ 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.

@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@cristim
cristim merged commit f5e706c into feat/multicloud-web-frontend Jun 8, 2026
5 checks passed
@cristim
cristim deleted the fix/min-savings-units branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant