Skip to content

fix(settings): clamp per-service payment options to supported combinations#30

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/service-term-payment-combos
Apr 25, 2026
Merged

fix(settings): clamp per-service payment options to supported combinations#30
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/service-term-payment-combos

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 24, 2026

Summary

Follow-up to #12 (Azure payment defaults, fixed in #27). The Settings → Purchasing form still let users set RDS 3-year + No Upfront (and the same for ElastiCache / OpenSearch / Redshift), which the AWS provider refuses at plan-execution time — see cmd/validators.go:warnRDS3YearNoUpfront. The plan-creation modal already guards against this via commitmentOptions.isValidCombination; this PR wires the same rules into the Settings form so a default that the backend can't honour can't even be typed in.

What changes

frontend/src/settings.ts

  • Adds syncPaymentConstraintsForService(field) and syncAllServiceCommitmentConstraints() — for each AWS service with a restricted pairing, hide + disable the invalid <option> rows in the payment dropdown. If the currently-selected payment becomes invalid after a term change, it auto-clamps to the first valid option so the form never holds an unsavable value.
  • Wires a change listener on each AWS service term dropdown → re-applies the constraint on that service's payment dropdown.
  • Calls syncAllServiceCommitmentConstraints() at the end of loadGlobalSettings so legacy-persisted invalid combos (RDS 3yr + no-upfront, possible on settings saved before this fix) get clamped on load.
  • Calls it from inside propagateTermToServices and propagatePaymentToServices so global-default propagation never silently writes invalid per-service combos.

Azure and GCP are intentionally out of scope — Azure has only two payment options (neither restricted by term) and GCP has no payment selector at all, so there are no same-shape bugs to fix there.

Test changes

New describe block per-service term/payment combination constraints:

  • RDS 3yr hides "No Upfront", keeps Partial / All Upfront selectable
  • RDS 1yr keeps all three payment options visible
  • Switching RDS term 1→3 with "No Upfront" selected auto-clamps to a valid payment
  • Legacy-persisted invalid combo (RDS 3yr + no-upfront) gets clamped on load
  • EC2 3yr keeps all three options visible (no service-level restriction)
  • ElastiCache / OpenSearch / Redshift all honour the same 3yr rule (via test.each)
  • Propagating global "no-upfront" to all services while term=3 clamps the restricted services and leaves EC2 unchanged

The existing "sets up default payment to propagate to AWS services" test was updated to reflect the new clamping behaviour — previously it asserted that RDS would accept the propagated no-upfront, which was exactly the bug this PR fixes.

Test fixture updated so the AWS payment selects carry all three options (no-upfront, partial-upfront, all-upfront) like production — the previous fixture only had two and couldn't meaningfully exercise the constraint filtering.

Test plan

  • npm test — 1243/1243 pass across 35 suites
  • npm run typecheck — clean
  • npm run build — production bundle builds
  • Browser smoke-test in the deployed Lambda preview after merge: open Settings → Purchasing, pick RDS card, flip term between 1yr / 3yr, confirm the "No Upfront" option disappears / reappears appropriately; repeat for ElastiCache / OpenSearch / Redshift; confirm EC2 shows all three options regardless of term.

Refs: #12

@coderabbitai review

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for additional AWS payment options with improved validation across services.
  • Bug Fixes

    • Payment options now automatically adjust when changing service terms.
    • Invalid persisted configurations are automatically corrected on startup.
    • Payment option visibility is properly enforced based on selected term combinations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 761de39d-53f6-4d7c-9e87-3480718ae3e1

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
📝 Walkthrough

Walkthrough

The PR introduces per-service AWS payment/term validity constraints in the settings UI. It adds payment option visibility rules (e.g., RDS hides no-upfront at 3-year term), auto-clamps invalid payments when term selections change, validates persisted configurations on load, and synchronizes constraints across bulk propagation operations.

Changes

Cohort / File(s) Summary
Test Updates
frontend/src/__tests__/settings.test.ts
Updated payment dropdown test fixtures to include partial-upfront option; expanded test suite to validate per-service payment/term constraint visibility, auto-clamping on term change, legacy invalid config handling, and propagation behavior across services with different restrictions.
Constraint Implementation
frontend/src/settings.ts
Added syncPaymentConstraintsForService helper to enforce per-service payment validity; wired term change handlers to trigger constraint sync; updated propagateTermToServices and propagatePaymentToServices to re-apply constraints after bulk operations; added full constraint sync to loadGlobalSettings to clamp invalid persisted combinations on initial load.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Settings UI
    participant Handler as Term/Payment Handler
    participant Validator as Constraint Validator
    participant DOM as Form DOM

    User->>UI: Change service term dropdown
    UI->>Handler: Trigger term change event
    Handler->>Validator: Call syncPaymentConstraintsForService(service)
    Validator->>Validator: Check isValidCombination for each payment option
    Validator->>DOM: Hide/disable invalid payment options
    Validator->>DOM: If current payment invalid, clamp to first valid option
    DOM->>User: Display updated payment options

    User->>UI: Propagate payment to all services
    UI->>Handler: Call propagatePaymentToServices(payment)
    Handler->>DOM: Update payment value for all service dropdowns
    Handler->>Validator: Sync constraints for each service
    Validator->>DOM: Hide/disable + clamp per-service invalid combinations
    DOM->>User: Display final valid state across all services
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Constraints now reign, payment terms align,
Service by service, invalid options resign,
No more persistence of configs gone wrong,
The dropdown dance is finally strong!
AWS settings, now sage and refined! ✨

🚥 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 and concisely summarizes the main change: fixing the settings form to properly clamp per-service payment options to valid AWS provider combinations.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/service-term-payment-combos

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 and usage tips.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 24, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/settings.ts (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Service key mismatch: savingsplans vs savings-plans.

SERVICE_FIELDS uses service: 'savingsplans' here, but commitmentOptions.ts registers the config under the key 'savings-plans' (with hyphen). When isValidCombination('aws', 'savingsplans', …) is called, the lookup fails and silently falls back to _default instead of the savings-plans-specific config.

Today both are identical (same terms and payments, no invalidCombinations), so constraint checking still works. However, if someone adds restrictions to the 'savings-plans' config in commitmentOptions.ts, this file will silently skip them. Fix by aligning the key:

Change SERVICE_FIELDS to use hyphenated form
-  { provider: 'aws', service: 'savingsplans', termId: 'aws-savingsplans-term', paymentId: 'aws-savingsplans-payment' },
+  { provider: 'aws', service: 'savings-plans', termId: 'aws-savingsplans-term', paymentId: 'aws-savingsplans-payment' },

The DOM ids (aws-savingsplans-*) can remain unchanged; only the service key needs aligning with commitmentOptions.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/settings.ts` at line 33, SERVICE_FIELDS currently uses service:
'savingsplans' which does not match the key 'savings-plans' registered in
commitmentOptions.ts, causing isValidCombination('aws','savingsplans',...) to
miss the specific config; update the SERVICE_FIELDS entry for AWS to use the
hyphenated service key 'savings-plans' (leave DOM ids like
'aws-savingsplans-term' unchanged) so lookups in commitmentOptions.ts and calls
to isValidCombination resolve to the correct config.
🧹 Nitpick comments (1)
frontend/src/__tests__/settings.test.ts (1)

735-862: Solid coverage for the new constraint wiring.

The suite exercises the three behavioral axes the implementation cares about:

  • Visibility differences by term (RDS 3yr vs 1yr).
  • Event-driven clamp on term change.
  • Load-time clamp for legacy-persisted invalid combos.
  • Negative case (EC2 is unrestricted).
  • .each for the other restricted services (elasticache/opensearch/redshift) avoids repetition.
  • Propagation path end-to-end (global default → per-service clamp).

Two minor optional refinements:

  1. The await Promise.resolve(); await Promise.resolve(); pump at lines 853–854 is brittle — if confirmAndPropagatePayment ever gains an extra await in its chain the test will silently pass before the assertion is meaningful. Prefer an explicit settle helper such as await new Promise(r => setTimeout(r, 0)); (matching what setupSettingsHandlers tests already use at lines 192/208/230).
  2. memorydb has the same { term: 3, payment: 'no-upfront' } restriction in commitmentOptions.ts but no entry in SERVICE_FIELDS, so it's correctly omitted from the .each. If a MemoryDB card is ever added to the Settings UI, extend the .each to cover it.
💚 Proposed test-settling tweak
-      // Allow the async confirmDialog promise to resolve.
-      await Promise.resolve();
-      await Promise.resolve();
+      // Allow the async confirmDialog → propagate chain to settle.
+      await new Promise((r) => setTimeout(r, 0));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/__tests__/settings.test.ts` around lines 735 - 862, Replace the
brittle double Promise.resolve pump after dispatching the defaultPayment change
with an explicit microtask tick (e.g. await new Promise(r => setTimeout(r, 0)))
so the test reliably waits for confirmAndPropagatePayment/confirmDialog promise
chains to settle; also, note that memorydb currently has the same restriction in
commitmentOptions.ts but is absent from SERVICE_FIELDS, so if a MemoryDB
settings card is later added extend the test.each([...]) that enumerates
'elasticache','opensearch','redshift' to include 'memorydb' to keep coverage
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend/src/settings.ts`:
- Line 33: SERVICE_FIELDS currently uses service: 'savingsplans' which does not
match the key 'savings-plans' registered in commitmentOptions.ts, causing
isValidCombination('aws','savingsplans',...) to miss the specific config; update
the SERVICE_FIELDS entry for AWS to use the hyphenated service key
'savings-plans' (leave DOM ids like 'aws-savingsplans-term' unchanged) so
lookups in commitmentOptions.ts and calls to isValidCombination resolve to the
correct config.

---

Nitpick comments:
In `@frontend/src/__tests__/settings.test.ts`:
- Around line 735-862: Replace the brittle double Promise.resolve pump after
dispatching the defaultPayment change with an explicit microtask tick (e.g.
await new Promise(r => setTimeout(r, 0))) so the test reliably waits for
confirmAndPropagatePayment/confirmDialog promise chains to settle; also, note
that memorydb currently has the same restriction in commitmentOptions.ts but is
absent from SERVICE_FIELDS, so if a MemoryDB settings card is later added extend
the test.each([...]) that enumerates 'elasticache','opensearch','redshift' to
include 'memorydb' to keep coverage consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5dc4bde3-5823-4b60-ab57-9e0290757e42

📥 Commits

Reviewing files that changed from the base of the PR and between f760d4d and a62260c.

📒 Files selected for processing (2)
  • frontend/src/__tests__/settings.test.ts
  • frontend/src/settings.ts

Two related fixes in the Settings → Purchasing form.

1. The per-service dropdowns let users pick AWS RDS 3-year + No Upfront,
   which the provider refuses (see `cmd/validators.go:warnRDS3YearNoUpfront`).
   The plan-creation modal already guarded against this through
   `commitmentOptions.isValidCombination`; the Settings form was the
   last place an invalid default could be persisted.

2. `commitmentOptions.ts` was marking ElastiCache / OpenSearch /
   Redshift / MemoryDB as also rejecting 3yr no-upfront, but that was
   over-cautious copy-paste. The backend validator only warns about
   RDS, and the newer `lib/purchase-compatibility.ts` calls RDS out as
   "the one hard rule". AWS does offer 3yr no-upfront on those other
   services, so the rules have been narrowed to RDS only.

Wiring:

- `settings.ts` gains `syncPaymentConstraintsForService(field)` and
  `syncAllServiceCommitmentConstraints()`. For each AWS service with an
  `invalidCombinations` entry (just RDS after this fix), payment
  `<option>` rows that pair invalidly with the currently-selected term
  are hidden + disabled. If the currently-selected payment becomes
  invalid after a term change, it auto-clamps to the first valid option
  so the form never holds an unsavable value.
- A `change` listener on each AWS service term `<select>` re-runs the
  sync on the matching payment select.
- `loadGlobalSettings` calls the sync at the end, so legacy-persisted
  invalid combos (RDS 3yr + no-upfront, possible on settings saved
  before this fix) get clamped on load.
- `propagateTermToServices` / `propagatePaymentToServices` both re-sync
  after the bulk write so global-default propagation never silently
  writes an invalid per-service combo.

Tests:

New `per-service term/payment combination constraints` describe block:
- RDS 3yr hides "No Upfront" and keeps Partial / All Upfront.
- RDS 1yr keeps all three payment options visible.
- Switching RDS term 1→3 with "No Upfront" selected auto-clamps.
- Legacy-persisted invalid combo (RDS 3yr + no-upfront) is clamped on
  load.
- EC2 3yr keeps all three options visible.
- ElastiCache / OpenSearch / Redshift 3yr all KEEP "No Upfront" visible
  and round-trip the persisted value cleanly (via test.each).
- Propagating global "no-upfront" to all services with term=3 clamps
  RDS specifically, leaves EC2 and the others unchanged.

Updated commitmentOptions.test.ts to assert ElastiCache / OpenSearch /
Redshift / MemoryDB have no `invalidCombinations` and accept 3yr
no-upfront through `isValidCombination`, `getValidPaymentOptions`, and
`getValidTermOptions`.

The existing "sets up default payment to propagate to AWS services"
test in settings.test.ts was updated to reflect the clamping behaviour
— previously it asserted RDS would accept propagated no-upfront, which
was the bug.

Test fixture updated so AWS payment selects carry all three options
like production — the previous two-option fixture couldn't meaningfully
exercise the constraint filtering.

Verified: `npm test` (1235/1235 pass across 35 suites), `npm run
typecheck` clean, `npm run build` emits a successful bundle.

Refs: #12
@cristim cristim force-pushed the fix/service-term-payment-combos branch from a62260c to 423b09b Compare April 24, 2026 14:32
cristim added a commit that referenced this pull request Apr 25, 2026
…opdown/DB (#63)

CodeRabbit flagged a key mismatch on PR #30: `SERVICE_FIELDS` in
`settings.ts` uses `service: 'savingsplans'`, while `commitmentConfigs`
in `commitmentOptions.ts` registered the AWS entry under `'savings-plans'`
(hyphenated). Any lookup via `isValidCombination`/`getCommitmentConfig`
with the non-hyphenated identifier silently fell through to `_default`
instead of hitting the Savings-Plans-specific config. Today both configs
are identical so behaviour is unchanged, but any future restriction
added to `'savings-plans'` would have been silently skipped.

CodeRabbit's suggestion was to rewrite `SERVICE_FIELDS` to the hyphenated
form, but `field.service` in `SERVICE_FIELDS` flows directly to
`api.updateServiceConfig(provider, service, cfg)` (settings.ts:1443)
which persists `service` as the primary key in the `service_configs`
table — existing production rows are `('aws', 'savingsplans')`, and
renaming SERVICE_FIELDS without a DB migration would have created
duplicate rows and orphaned the old config. The dropdown at
`index.html:112,703` (`<option value="savingsplans">`) and the backend
CLI flag (`cmd/main.go:92`) also use the non-hyphenated form.

Fix by renaming the `commitmentConfigs.aws` key from `'savings-plans'`
to `savingsplans` so every frontend call site — the SERVICE_FIELDS
path, the plan-form dropdown path, and any direct lookups — resolves
to the Savings-Plans-specific config. Update the two test cases in
`commitmentOptions.test.ts` to match.

This addresses the only live CodeRabbit finding across all open/closed
PRs (24-39, 52-54); PR #52's nitpicks were already resolved in db429fe,
and the remaining PR #30 nitpicks (double `Promise.resolve()` in
settings.test.ts:854, memorydb absent from SERVICE_FIELDS) apply only
to code that lives on PR #30's branch and not the base.
@cristim cristim merged commit 9b6fc9d into feat/multicloud-web-frontend Apr 25, 2026
3 checks passed
@cristim cristim added type/bug Defect severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days priority/p2 Backlog-worthy triaged Item has been triaged labels Apr 28, 2026
@cristim cristim deleted the fix/service-term-payment-combos branch April 29, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/high Significant 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