Skip to content

fix(api/validation): tighten purchasePaymentWhitelist to provider-canonical sets (followup to #709) #717

Description

@cristim

Symptom

After PR #709 tightened the plan validator (internal/config/validation.go) to reject cross-provider payment-option tokens (e.g. all-upfront on a GCP service), the purchase-execute validator (internal/api/validation.go:purchasePaymentWhitelist) still accepts the full union {all-upfront, upfront, no-upfront, monthly} for every provider — so a token that's now rejected at plan-creation time can still slip through at purchase-execute time via a different code path.

Why the gap exists

internal/api/validation.go:purchasePaymentWhitelist is a permissive boundary check that was correct under the original assumption "all providers accept all AWS-style tokens." After #709 made the validator provider-aware, this whitelist is the only path that still treats payment tokens as provider-agnostic.

What we want

Mirror PR #709's approach at the internal/api/ boundary:

  1. Replace the single purchasePaymentWhitelist slice with a provider-keyed lookup that uses the same ValidPaymentOptionsByProvider map from internal/config/validation.go (the canonical source — single point of truth, no drift between the two validators).
  2. Tighten the error message to include the provider in the rejection reason, matching the plan-validator's shape:
    invalid payment option for azure service: "all-upfront" (valid for azure: upfront, monthly)
    
  3. Apply the same NormalizePaymentOption alias-coercion (already added in PR fix(config): accept provider-canonical payment options in plan validator #709 — internal/config/validation.go) at this boundary too, so the API accepts legacy AWS-style tokens at ingest and canonicalizes them BEFORE the whitelist check rejects them. The two layers should mirror each other.

Sets to enforce (verified by PR #709 against the actual provider switches)

  • AWS: {no-upfront, partial-upfront, all-upfront}
  • Azure: {upfront, monthly} (verified against all 7 Azure service-client switches at providers/azure/services/{compute,cache,cosmosdb,database,search,synapse,managedredis}/client.go)
  • GCP: {monthly} only (GCP CUDs are billed monthly across the term — buildCommitmentRequests at providers/gcp/services/computeengine/client.go:350-373 takes only a Plan and never reads PaymentOption)

Tests

  • Update existing purchasePaymentWhitelist tests to use the provider-keyed shape.
  • Add cases: all-upfront on Azure → rejected with provider-specific error; monthly on Azure → accepted; all-upfront on GCP → rejected; upfront on GCP → rejected (GCP is monthly-only).
  • After NormalizePaymentOption is applied at ingest, the legacy AWS-style tokens should be coerced (all-upfront on Azure → upfront, no-upfront on GCP → monthly) — verify the coerce-then-validate ordering preserves the existing API contract for legacy callers.

Related

Why this matters

Without this fix, a token that's rejected at plan-creation can be silently coerced (or worse, rejected with a confusing error) at purchase-execute. The two boundaries should report the same error for the same input. Operator confidence in the validator depends on consistent behaviour across layers.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/sHoursimpact/manyAffects most userspr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)pr-mergedThe PR for this issue has been mergedpriority/p2Backlog-worthyseverity/mediumModerate harmtriagedItem has been triagedtype/bugDefecturgency/this-sprintWithin the current sprint

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions