Skip to content

chore(common): normalize Savings Plans identifier to "savingsplans" (frontend canonical)#94

Open
cristim wants to merge 1 commit intofeat/multicloud-web-frontendfrom
chore/normalize-savings-plans-identifier
Open

chore(common): normalize Savings Plans identifier to "savingsplans" (frontend canonical)#94
cristim wants to merge 1 commit intofeat/multicloud-web-frontendfrom
chore/normalize-savings-plans-identifier

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 26, 2026

Summary

Normalises common.ServiceSavingsPlans from the hyphenated "savings-plans" to the frontend canonical "savingsplans" so that direct comparisons (rec.Service == common.ServiceSavingsPlans) work without the historical normaliser shim. Picks issue #85 Option B (frontend wins) to avoid a SQL data migration on service_configs.service / purchase_history.service.

Refs #85 (the issue's hard removal of the dual-case branch is intentionally deferred — see "Why Refs not Closes" below).

Pre-flight findings

Tree-wide grep for savings-plans / savingsplans / ServiceSavingsPlans:

Surface Form used Action
Frontend (frontend/src/**) "savingsplans" everywhere no change
Frontend dropdown / SERVICE_FIELDS / commitmentConfigs "savingsplans" no change
Backend constant pkg/common/types.go:49 was "savings-plans" flipped to "savingsplans"
Backend constant test pkg/common/types_test.go:42 asserted "savings-plans" flipped + regression-guard comment
internal/purchase/execution.go:384 dual-case "savings-plans", "savingsplans" retained (see below)
internal/purchase/coverage_extra_test.go both inputs map to constant retained
Every other Go callsite (40+) uses the constant common.ServiceSavingsPlans symbolically unchanged — compiles against new value automatically
cmd/main.go::parseServices map already "savingsplans" no change
DB migrations / schema service VARCHAR(64), no CHECK, no seed data with either form no migration needed
Audit log writes (pkg/common/audit.go) write-only, never read back into Go safe

Why the dual-case in execution.go is retained (deferred from #85's acceptance)

scheduler.go:834 and audit.go:50 serialise rec.Service as string(rec.Service) into the purchase_executions.recommendations JSONB column. Any Lambda-scheduled Savings Plans execution persisted before this PR carries "service": "savings-plans" in that JSONB blob. On retry / approval (internal/purchase/execution.go:231 iterates exec.Recommendations), each rec is re-fed through mapServiceType(). Removing the legacy alias arm now would silently break those rows by mapping them to ServiceType("savings-plans") instead of ServiceSavingsPlans, after which cloudProvider.GetServiceClient(ctx, serviceType, …) would fail.

Trade-off:

  • Short-term: keep the dual-case arm with a clear comment, a TODO(#85) marker, and a follow-up issue tracking eventual removal once historical rows have aged out (~6 months retention). The constant is canonical; the alias is purely a read-side shim at the API boundary.
  • Alternative considered: a one-shot SQL UPDATE rewriting purchase_executions.recommendations JSONB to flip "savings-plans""savingsplans". Out of scope for this PR (matches issue Normalize 'savings-plans' vs 'savingsplans' identifier across frontend + backend #85's explicit "Option B" preference of avoiding migrations).

The regression test in pkg/common/types_test.go will fail if anyone flips the constant back, so the canonical never silently drifts again.

Changes

  • pkg/common/types.goServiceSavingsPlans = "savingsplans"; expanded godoc explains rationale + points back at Normalize 'savings-plans' vs 'savingsplans' identifier across frontend + backend #85.
  • pkg/common/types_test.go — regression guard asserts "savingsplans".
  • internal/purchase/execution.gomapServiceType keeps both "savingsplans" and "savings-plans" with a documented backwards-compat block + TODO(#85) for removal.
  • internal/purchase/coverage_extra_test.go — legacy "savings-plans" input case retained and re-documented.

Net diff: 30 insertions, 5 deletions across 4 files.

Verification

  • go build ./... clean from the root and from each submodule (pkg/, providers/aws, providers/azure, providers/gcp).
  • go test ./... from root + pkg/ submodule: all tests pass that were passing before.
  • go test ./providers/aws/...: the only AWS-side failures are TestAWSProvider_GetDefaultRegion/*, which are pre-existing environment-leak bugs unrelated to this PR (the test mutates aws.Config.Region but IsConfigured() clobbers it from the SDK default-credentials chain reading ~/.aws/config). TestSavingsPlans* and TestAWSProvider_GetSupportedServices (which touches ServiceSavingsPlans directly) all pass.
  • go test ./providers/gcp/...: pre-existing failure in TestMemorystoreClient_GetExistingCommitments_WithMockService is unrelated to Savings Plans.
  • go vet ./... clean.
  • Pre-commit hooks (gofmt, go mod tidy, go vet, gosec, trivy, AWS secrets scan, cyclomatic complexity, full Go test suite) all pass.

Why Refs #85 not Closes #85

Issue #85's acceptance includes "The dual-case branch in execution.go:384 removed." This PR retains the dual-case as a backwards-compat alias for persisted Lambda-scheduled rows (rationale above). The follow-up issue tracking the eventual removal will be filed once this lands and the post-merge verification completes.

Test plan

  • CI green
  • CodeRabbit review settles
  • Post-merge: deploy to lambda URL, verify the Savings Plans dropdown still saves to service_configs (it should — frontend was already writing "savingsplans"); the round-trip read should now compare equal to common.ServiceSavingsPlans without the normaliser

…frontend canonical)

Aligns common.ServiceSavingsPlans with the value the frontend already
persists ("savingsplans") and removes the foot-gun where any new code
comparing rec.Service == common.ServiceSavingsPlans would silently miss
Savings Plans rows. Picks the frontend canonical (issue #85 Option B) to
avoid a SQL data migration on service_configs.service / purchase_history.

Pre-flight findings (grep over the whole tree):
- frontend already uses "savingsplans" everywhere — no frontend changes.
- the only Go string literals using the hyphenated form were the
  constant declaration, its constant-value test, the dual-case branch in
  the purchase-execution mapper, and the matching test case for that
  mapper.
- service_configs.service is VARCHAR(64) with no CHECK constraint and no
  seed data; the frontend has always written "savingsplans".
- purchase_executions.recommendations is JSONB and rec.Service is
  serialised as `string(rec.Service)` (scheduler.go, audit.go), so any
  Lambda-scheduled SP execution persisted before this change carries
  rec.Service == "savings-plans" in the JSONB blob and is re-fed through
  mapServiceType on retry / approval. The dual-case in execution.go is
  therefore retained as a backwards-compat alias (with a TODO and a
  follow-up issue) instead of being removed; the persistent rows are the
  reason the issue's hard "remove the dual-case branch" couldn't be
  satisfied in a single PR.

Changes:
- pkg/common/types.go: ServiceSavingsPlans value flipped from
  "savings-plans" to "savingsplans"; expanded godoc explains the
  rationale and points back at the issue.
- pkg/common/types_test.go: regression guard now asserts the constant
  value is "savingsplans" so any future flip back is a deliberate change
  that requires a SQL migration.
- internal/purchase/execution.go: mapServiceType keeps both
  "savingsplans" and "savings-plans" as legacy alias, with a comment
  block + TODO(#85) explaining when it can be removed.
- internal/purchase/coverage_extra_test.go: legacy "savings-plans" input
  case retained and re-documented.

Refs #85
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

@cristim has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 41 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 31 minutes and 41 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8737d34-c3e0-4ac3-b2db-fd2e82b714e1

📥 Commits

Reviewing files that changed from the base of the PR and between 2e33c88 and 0b321f3.

📒 Files selected for processing (4)
  • internal/purchase/coverage_extra_test.go
  • internal/purchase/execution.go
  • pkg/common/types.go
  • pkg/common/types_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/normalize-savings-plans-identifier

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 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 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.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 28, 2026

Merge conflict detected (mergeStateStatus: DIRTY) — rebase needed. The dual-case backwards-compat alias in execution.go is intentionally retained (documented reasoning re: persisted JSONB rows); this is correct per the Refs #85 (not Closes) framing. (triage agent wave2-E)

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/chore Maintenance / non-user-visible urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant