From 09730be3fc053ccaea08c28e3fb41178ab4748f5 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 15 Oct 2026 11:19:50 +0200 Subject: [PATCH] feat(gcp): retain checked catalog commitment currency Copy the checked pricing currency beside known commitment amounts. Leave failed or incomplete catalog prices unknown and keep savings currency separate. Pin the published shared contract for standalone use. Closes #411 --- providers/gcp/go.mod | 2 +- providers/gcp/go.sum | 4 ++-- providers/gcp/services/cloudsql/client.go | 1 + .../gcp/services/cloudsql/client_test.go | 11 ++++++++--- .../cloudsql/pricing_currency_test.go | 8 ++++++++ .../gcp/services/computeengine/client.go | 1 + .../gcp/services/computeengine/client_test.go | 9 ++++++++- .../services/computeengine/pricing_test.go | 19 +++++++++++++++++++ providers/gcp/services/memorystore/client.go | 1 + .../gcp/services/memorystore/client_test.go | 15 +++++++++++++-- .../memorystore/pricing_currency_test.go | 8 ++++++++ 11 files changed, 70 insertions(+), 9 deletions(-) diff --git a/providers/gcp/go.mod b/providers/gcp/go.mod index b09ba6d6..58bbf9a2 100644 --- a/providers/gcp/go.mod +++ b/providers/gcp/go.mod @@ -8,7 +8,7 @@ require ( cloud.google.com/go/redis v1.18.3 cloud.google.com/go/resourcemanager v1.10.7 cloud.google.com/go/storage v1.59.2 - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013221254-77bc8a993eb8 + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015085730-498b74f31f01 github.com/googleapis/gax-go/v2 v2.21.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 diff --git a/providers/gcp/go.sum b/providers/gcp/go.sum index 9ba08105..cb059dab 100644 --- a/providers/gcp/go.sum +++ b/providers/gcp/go.sum @@ -36,8 +36,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013221254-77bc8a993eb8 h1:0EMP5PHw8RnQ5Xl8B2firZ8CwRSpmxitMZ4cdYuL8p4= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013221254-77bc8a993eb8/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015085730-498b74f31f01 h1:R2EQAvwLAxPrdlZXmF+bI7ioQYh1CxBkethx5hq6WWA= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015085730-498b74f31f01/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= diff --git a/providers/gcp/services/cloudsql/client.go b/providers/gcp/services/cloudsql/client.go index cf61ee3a..d80ae5f1 100644 --- a/providers/gcp/services/cloudsql/client.go +++ b/providers/gcp/services/cloudsql/client.go @@ -527,6 +527,7 @@ func (c *Client) fillSQLPricing(ctx context.Context, rec *common.Recommendation, if pricing.CommitmentPrice > 0 { monthly := pricing.CommitmentPrice / float64(termYears*12) rec.RecurringMonthlyCost = &monthly + rec.CommitmentCurrency = pricing.Currency } rec.OnDemandCost = pricing.OnDemandPrice rec.SavingsPercentage = pricing.SavingsPercentage diff --git a/providers/gcp/services/cloudsql/client_test.go b/providers/gcp/services/cloudsql/client_test.go index 64b62e25..ae19233f 100644 --- a/providers/gcp/services/cloudsql/client_test.go +++ b/providers/gcp/services/cloudsql/client_test.go @@ -890,7 +890,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing UnitPrice: &cloudbilling.Money{ Units: 0, Nanos: int64(onDemandHourly * 1e9), - CurrencyCode: "USD", + CurrencyCode: "EUR", }, }, }, @@ -912,7 +912,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing UnitPrice: &cloudbilling.Money{ Units: 0, Nanos: int64(commitmentHourly * 1e9), - CurrencyCode: "USD", + CurrencyCode: "EUR", }, }, }, @@ -931,7 +931,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing Category: recommenderpb.Impact_COST, Projection: &recommenderpb.Impact_CostProjection{ CostProjection: &recommenderpb.CostProjection{ - Cost: &money.Money{Units: -100, CurrencyCode: "USD"}, + Cost: &money.Money{Units: -100, CurrencyCode: "JPY"}, }, }, }, @@ -952,6 +952,8 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing rec := client.convertGCPRecommendation(ctx, gcpRec, common.RecommendationParams{Term: term, PaymentOption: payment}) rec.Count = 3 rec.AccountName = "fixture" + assert.Equal(t, "EUR", rec.CommitmentCurrency) + assert.Equal(t, float64(100), rec.EstimatedSavings) require.NotNil(t, rec.RecurringMonthlyCost) assert.Zero(t, rec.CommitmentCost) assert.InDelta(t, 100, *rec.RecurringMonthlyCost, 0.00001) @@ -968,6 +970,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing scaled, err := common.ResizeCount(*rec, 2) require.NoError(t, err) assert.Equal(t, 2, scaled.Count) + assert.Equal(t, "EUR", scaled.CommitmentCurrency) assert.InDelta(t, 800*months/12, scaled.CommitmentCost+*scaled.RecurringMonthlyCost*months, 0.0001) assert.InDelta(t, 100, *rec.RecurringMonthlyCost, 0.00001) quote, err := client.GetOfferingDetails(ctx, *rec) @@ -984,6 +987,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing assert.Zero(t, rec.CommitmentCost) assert.Zero(t, rec.BreakEvenMonths) assert.Nil(t, rec.RecurringMonthlyCost) + assert.Empty(t, rec.CommitmentCurrency) }) } @@ -1012,6 +1016,7 @@ func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost_BillingFai assert.Zero(t, rec.CommitmentCost) assert.Zero(t, rec.BreakEvenMonths) assert.Nil(t, rec.RecurringMonthlyCost) + assert.Empty(t, rec.CommitmentCurrency) }) } } diff --git a/providers/gcp/services/cloudsql/pricing_currency_test.go b/providers/gcp/services/cloudsql/pricing_currency_test.go index 0b5c977e..186e438c 100644 --- a/providers/gcp/services/cloudsql/pricing_currency_test.go +++ b/providers/gcp/services/cloudsql/pricing_currency_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/api/cloudbilling/v1" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" ) func currencySKU(id, description, usageType, region, currency string, nanos int64) *cloudbilling.Sku { @@ -51,13 +53,19 @@ func TestGetSQLPricing_Currency(t *testing.T) { }}}) got, err := client.getSQLPricing(ctx, tier, region, 1) + rec := common.Recommendation{ResourceType: tier, Term: "1yr"} + client.fillSQLPricing(ctx, &rec, 1) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) + assert.Empty(t, rec.CommitmentCurrency) + assert.Nil(t, rec.RecurringMonthlyCost) return } require.NoError(t, err) assert.Equal(t, tt.wantCurrency, got.Currency) + assert.Equal(t, tt.wantCurrency, rec.CommitmentCurrency) + require.NotNil(t, rec.RecurringMonthlyCost) }) } } diff --git a/providers/gcp/services/computeengine/client.go b/providers/gcp/services/computeengine/client.go index 4e029b19..9d49812e 100644 --- a/providers/gcp/services/computeengine/client.go +++ b/providers/gcp/services/computeengine/client.go @@ -1168,6 +1168,7 @@ func (c *Client) enrichRecWithPricing(ctx context.Context, rec *common.Recommend termMonths := float64(termYears * 12) monthly := pricing.CommitmentPrice / termMonths rec.RecurringMonthlyCost = &monthly + rec.CommitmentCurrency = pricing.Currency rec.OnDemandCost = pricing.OnDemandPrice / termMonths rec.SavingsPercentage = pricing.SavingsPercentage } diff --git a/providers/gcp/services/computeengine/client_test.go b/providers/gcp/services/computeengine/client_test.go index 4409efe3..6fb3af8d 100644 --- a/providers/gcp/services/computeengine/client_test.go +++ b/providers/gcp/services/computeengine/client_test.go @@ -1342,12 +1342,19 @@ func TestConverterToInsert_CountNonZero_VCPUAmountSet(t *testing.T) { func TestConverterFillsPricingForScorer(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") - client.SetBillingService(mockBillingWithCommitment()) + billing := mockBillingWithCommitment() + for _, sku := range billing.skus.Skus { + sku.PricingInfo[0].PricingExpression.TieredRates[0].UnitPrice.CurrencyCode = "EUR" + } + client.SetBillingService(billing) gcpRec := realisticCUDRecommendation() + gcpRec.PrimaryImpact.GetCostProjection().Cost.CurrencyCode = "JPY" rec, err := client.convertGCPRecommendation(ctx, gcpRec, common.RecommendationParams{}) require.NoError(t, err) require.NotNil(t, rec) + assert.Equal(t, "EUR", rec.CommitmentCurrency) + assert.Equal(t, float64(200), rec.EstimatedSavings) // C2: SavingsPercentage must be > 0 so a MinSavingsPct filter doesn't silently drop the rec. require.Greater(t, rec.SavingsPercentage, float64(0), diff --git a/providers/gcp/services/computeengine/pricing_test.go b/providers/gcp/services/computeengine/pricing_test.go index 33b32c0f..2ba0ed9d 100644 --- a/providers/gcp/services/computeengine/pricing_test.go +++ b/providers/gcp/services/computeengine/pricing_test.go @@ -149,9 +149,28 @@ func TestEnrichRecWithPricing_MonthlyFiguresPerTerm(t *testing.T) { client.enrichRecWithPricing(ctx, &rec) assert.Zero(t, rec.CommitmentCost) + assert.Equal(t, "USD", rec.CommitmentCurrency) require.NotNil(t, rec.RecurringMonthlyCost) assert.InDelta(t, tc.commitHourly*730, *rec.RecurringMonthlyCost, 1e-6) assert.InDelta(t, tc.odHourly*730, rec.OnDemandCost, 1e-6, "OnDemandCost is the monthly on-demand price") }) } } + +func TestEnrichRecWithPricing_InvalidCurrency(t *testing.T) { + for _, currency := range []string{"", "EUR"} { + t.Run("commitment currency "+currency, func(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, "test-project", "us-central1") + require.NoError(t, err) + skus := n2AmericasSKUs() + skus[2].PricingInfo[0].PricingExpression.TieredRates[0].UnitPrice.CurrencyCode = currency + client.SetBillingService(&MockBillingService{skus: &cloudbilling.ListSkusResponse{Skus: skus}}) + rec := newRec("n2-standard-4", 4, 16) + rec.Term = "1yr" + client.enrichRecWithPricing(ctx, &rec) + assert.Empty(t, rec.CommitmentCurrency) + assert.Nil(t, rec.RecurringMonthlyCost) + }) + } +} diff --git a/providers/gcp/services/memorystore/client.go b/providers/gcp/services/memorystore/client.go index 80ceb12c..cddd7c8c 100644 --- a/providers/gcp/services/memorystore/client.go +++ b/providers/gcp/services/memorystore/client.go @@ -495,6 +495,7 @@ func (c *Client) fillRedisPricing(ctx context.Context, rec *common.Recommendatio if pricing.CommitmentPrice > 0 { monthly := pricing.CommitmentPrice / float64(termYears*12) rec.RecurringMonthlyCost = &monthly + rec.CommitmentCurrency = pricing.Currency } rec.OnDemandCost = pricing.OnDemandPrice rec.SavingsPercentage = pricing.SavingsPercentage diff --git a/providers/gcp/services/memorystore/client_test.go b/providers/gcp/services/memorystore/client_test.go index c7d44875..5c00e5b6 100644 --- a/providers/gcp/services/memorystore/client_test.go +++ b/providers/gcp/services/memorystore/client_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/iterator" + "google.golang.org/genproto/googleapis/type/money" "github.com/LeanerCloud/cloud-commitments-go/pkg/common" "github.com/LeanerCloud/cloud-commitments-go/pkg/reporter" @@ -837,7 +838,7 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test UnitPrice: &cloudbilling.Money{ Units: 0, Nanos: int64(onDemandHourly * 1e9), - CurrencyCode: "USD", + CurrencyCode: "EUR", }, }, }, @@ -859,7 +860,7 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test UnitPrice: &cloudbilling.Money{ Units: 0, Nanos: int64(commitmentHourly * 1e9), - CurrencyCode: "USD", + CurrencyCode: "EUR", }, }, }, @@ -874,6 +875,11 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test gcpRec := &recommenderpb.Recommendation{ Name: "test-rec", + PrimaryImpact: &recommenderpb.Impact{ + Projection: &recommenderpb.Impact_CostProjection{ + CostProjection: &recommenderpb.CostProjection{Cost: &money.Money{Units: -100, CurrencyCode: "JPY"}}, + }, + }, Content: &recommenderpb.RecommendationContent{ OperationGroups: []*recommenderpb.OperationGroup{ { @@ -891,6 +897,8 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test rec := client.convertGCPRecommendation(ctx, gcpRec, common.RecommendationParams{Term: term, PaymentOption: payment}) rec.Count = 3 rec.AccountName = "fixture" + assert.Equal(t, "EUR", rec.CommitmentCurrency) + assert.Equal(t, float64(100), rec.EstimatedSavings) require.NotNil(t, rec.RecurringMonthlyCost) assert.Zero(t, rec.CommitmentCost) assert.InDelta(t, 100, *rec.RecurringMonthlyCost, 0.00001) @@ -907,6 +915,7 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test scaled, err := common.ResizeCount(*rec, 2) require.NoError(t, err) assert.Equal(t, 2, scaled.Count) + assert.Equal(t, "EUR", scaled.CommitmentCurrency) assert.InDelta(t, 800*months/12, scaled.CommitmentCost+*scaled.RecurringMonthlyCost*months, 0.0001) assert.InDelta(t, 100, *rec.RecurringMonthlyCost, 0.00001) quote, err := client.GetOfferingDetails(ctx, *rec) @@ -923,6 +932,7 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *test assert.Zero(t, rec.CommitmentCost) assert.Zero(t, rec.BreakEvenMonths) assert.Nil(t, rec.RecurringMonthlyCost) + assert.Empty(t, rec.CommitmentCurrency) }) } @@ -950,6 +960,7 @@ func TestMemorystoreClient_ConvertGCPRecommendation_RecurringMonthlyCost_Billing assert.Zero(t, rec.CommitmentCost) assert.Zero(t, rec.BreakEvenMonths) assert.Nil(t, rec.RecurringMonthlyCost) + assert.Empty(t, rec.CommitmentCurrency) }) } } diff --git a/providers/gcp/services/memorystore/pricing_currency_test.go b/providers/gcp/services/memorystore/pricing_currency_test.go index 92ff7cd9..ae4777b6 100644 --- a/providers/gcp/services/memorystore/pricing_currency_test.go +++ b/providers/gcp/services/memorystore/pricing_currency_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/api/cloudbilling/v1" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" ) func currencySKU(id, description, usageType, region, currency string, nanos int64) *cloudbilling.Sku { @@ -51,13 +53,19 @@ func TestGetRedisPricing_Currency(t *testing.T) { }}}) got, err := client.getRedisPricing(ctx, tier, region, 1) + rec := common.Recommendation{ResourceType: tier, Term: "1yr"} + client.fillRedisPricing(ctx, &rec, 1) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) + assert.Empty(t, rec.CommitmentCurrency) + assert.Nil(t, rec.RecurringMonthlyCost) return } require.NoError(t, err) assert.Equal(t, tt.wantCurrency, got.Currency) + assert.Equal(t, tt.wantCurrency, rec.CommitmentCurrency) + require.NotNil(t, rec.RecurringMonthlyCost) }) } }