Repository navigation
fix(azure): retail-price currency is a USD seed overwritten by whichever item came last #102
Description
Summary
All seven Azure retail-price extractors seed currency = "USD" when the API reports none, then let every item with a non-empty CurrencyCode overwrite it, while onDemand and reservation are assigned only from items matching the term or Type == "Consumption". The returned currency is therefore whatever the last item carried, not the currency of the two prices actually returned, and a response mixing currencies produces a savings percentage computed across denominations and labelled with one of them. No client pins currencyCode in the request (params.Add sets only $filter and api-version in all seven), so the code relies on the Retail Prices API's undocumented USD default to keep the item set single-currency. The same seed-and-overwrite shape is copied into the GCP cloudstorage, cloudsql and memorystore clients. OfferingDetails.Currency, the field this label ends up in, currently has no consumer, so the spend-cap half of the original finding is not established; the mislabelled savings percentage on the recommendation path is. LeanerCloud/cloud-commitments-cli#1934 covers the price last-item-wins in the same loop; this is the currency half its fix does not address.
Location
providers/azure/services/cache/client.go:600-612at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (savings percentage from the pair at:558, currency stamped at:564)providers/azure/services/compute/client.go:766-778providers/azure/services/cosmosdb/client.go:598-610providers/azure/services/database/client.go:618-630providers/azure/services/managedredis/client.go:520-529(parsePriceItems)providers/azure/services/search/client.go:511-523providers/azure/services/synapse/client.go:457-475- GCP copies:
providers/gcp/services/cloudstorage/client.go:388,:400-402, and the same incloudsqlandmemorystore
Failure scenario
A Retail Prices response whose Consumption item is USD and whose later reservation item is EUR. The function returns onDemand in USD, reservation in EUR, labels the pair EUR, and getRedisPricing computes savingsPercentage by mixing the two, reporting a fabricated saving on the recommendation with no signal.
Evidence
currency = "USD"
termStr := azureTermString(termYears)
for _, item := range items {
if item.CurrencyCode != "" {
currency = item.CurrencyCode
}
if item.ReservationTerm == termStr {
reservation = item.RetailPrice
} else if item.Type == "Consumption" {
onDemand = item.UnitPrice
}
}Suggested fix
Pin currencyCode=USD in the request params, take currency only from the items the two prices were read from, and return an error when those items carry no currency or disagree, instead of seeding USD and labelling a mixed pair.
Found by the 2026-09-02 codebase audit, findings A15-009, A08b-011, each reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md.
Activity
cristim commentedon Oct 16, 2026
Checked against main
740dfbd: the Azure extractors now take currency from the selected item viapricing.SelectReservation, and GCP Cloud SQL and Memorystore usebillingcurrency.Unify. The only remaining USD seed isproviders/gcp/services/cloudstorage/client.go:283, which open PR #424 changes. This can close once #424 merges.