Repository navigation
fix(api/validation): tighten purchasePaymentWhitelist to provider-canonical sets (followup to #709) #717
Description
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:
- Replace the single
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). - 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
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)
- AWS:
{no-upfront, partial-upfront, all-upfront} - Azure:
{upfront, monthly}(verified against all 7 Azure service-client switches atproviders/azure/services/{compute,cache,cosmosdb,database,search,synapse,managedredis}/client.go) - GCP:
{monthly}only (GCP CUDs are billed monthly across the term —buildCommitmentRequestsatproviders/gcp/services/computeengine/client.go:350-373takes only aPlanand never readsPaymentOption)
Tests
- Update existing
purchasePaymentWhitelisttests to use the provider-keyed shape. - Add cases:
all-upfronton Azure → rejected with provider-specific error;monthlyon Azure → accepted;all-upfronton GCP → rejected;upfronton GCP → rejected (GCP is monthly-only). - After
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
- PR fix(config): accept provider-canonical payment options in plan validator #709 (plan-validator tightening — the model)
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.
Activity
docs(db): clarify GCP commitments are monthly + nil round-trip test
feat(db): nullable MonthlyCost on PurchaseHistoryRecord (closes #255) (…
fix(api/validation): provider-canonical payment-option whitelist (#717)…
cristim commentedon Jul 27, 2026
Verification sweep against
main(101f099fb) on 2026-07-27 finds this already resolved.The payment-option whitelist is now provider-canonical via config.ValidPaymentOptionsByProvider.
Evidence: commit dceb8b0 (PR #793).
Recommending close.
cristim commentedon Sep 2, 2026
Verified resolved at 3c0f8ac: the purchase-execute boundary now validates payment options against config.ValidPaymentOptionsByProvider, applies NormalizePaymentOption alias coercion first, and reports the provider in the rejection message, mirroring the plan validator. Evidence: internal/api/validation.go:504-632 (#793). Residual axes checked: legacy AWS-token coercion ordering, per-provider sets for AWS/Azure/GCP; none found. Closing as completed; reopen if the behaviour recurs.