diff --git a/providers/gcp/go.mod b/providers/gcp/go.mod index b09ba6d..58bbf9a 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 9ba0810..cb059da 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 cf61ee3..d80ae5f 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 64b62e2..ae19233 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 0b5c977..186e438 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 4e029b1..9d49812 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 4409efe..6fb3af8 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 33b32c0..2ba0ed9 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 80ceb12..cddd7c8 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 c7d4487..5c00e5b 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 92ff7cd..ae4777b 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) }) } }