You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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).
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)
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.
internal/config/validation.go:ValidPaymentOptionsByProvider (single source of truth — re-export from here)
internal/config/validation.go:NormalizePaymentOption (alias-coercion helper to reuse)
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.
Symptom
After PR #709 tightened the plan validator (
internal/config/validation.go) to reject cross-provider payment-option tokens (e.g.all-upfronton 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:purchasePaymentWhitelistis 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:purchasePaymentWhitelistslice with a provider-keyed lookup that uses the sameValidPaymentOptionsByProvidermap frominternal/config/validation.go(the canonical source — single point of truth, no drift between the two validators).NormalizePaymentOptionalias-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)
{no-upfront, partial-upfront, all-upfront}{upfront, monthly}(verified against all 7 Azure service-client switches atproviders/azure/services/{compute,cache,cosmosdb,database,search,synapse,managedredis}/client.go){monthly}only (GCP CUDs are billed monthly across the term —buildCommitmentRequestsatproviders/gcp/services/computeengine/client.go:350-373takes only aPlanand never readsPaymentOption)Tests
purchasePaymentWhitelisttests to use the provider-keyed shape.all-upfronton Azure → rejected with provider-specific error;monthlyon Azure → accepted;all-upfronton GCP → rejected;upfronton GCP → rejected (GCP is monthly-only).NormalizePaymentOptionis applied at ingest, the legacy AWS-style tokens should be coerced (all-upfronton Azure →upfront,no-upfronton GCP →monthly) — verify the coerce-then-validate ordering preserves the existing API contract for legacy callers.Related
internal/config/validation.go:ValidPaymentOptionsByProvider(single source of truth — re-export from here)internal/config/validation.go:NormalizePaymentOption(alias-coercion helper to reuse)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.