Repository: cycloidio/terracost
Pull request #49 (2021-07-07 09:43:47)

cost: include currency in cost calculations 
This PR resolves the issue #48 . It adds currency in cost and checks if the currency is the same when making calculations. Currency is extracted from the ingested data - from the database. 

theskch: IMHO using the https://pkg.go.dev/golang.org/x/text/currency#ParseISO is a good suggestion, I would expect all CP to have a valid currency format, and setting currencies for all CP to the same format before inserting them in the DB will guarantee there is no mismatch because of the format.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05251e0..98bd0f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## [Unreleased]
 
+### Added
+
+- Include currency in resource component cost estimation
+  ([Issue #48](https://github.com/cycloidio/terracost/issues/48))
 
 ## [0.3.0] _2021-06-01_
 
diff --git a/README.md b/README.md
index 88cf90a..45e09e8 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ backend := mysql.NewBackend(db)
 
 // service can be "AmazonEC2" or "AmazonRDS"
 // region is any AWS region, e.g. "us-east-1" or "eu-west-3"
-ingester := aws.NewIngester(service, region)
+ingester, err := aws.NewIngester(service, region)
 err = terracost.IngestPricing(ctx, backend, ingester)
 ```
 
@@ -67,7 +67,7 @@ go func() {
 2. Initialize an ingester capable of tracking progress (in this example the channel will receive an update every 5 seconds):
 
 ```go
-ingester := aws.NewIngester(service, region, aws.WithProgress(progressCh, 5*time.Second))
+ingester, err := aws.NewIngester(service, region, aws.WithProgress(progressCh, 5*time.Second))
 ```
 
 3. Use the ingester as in the previous section.
diff --git a/cost/cost.go b/cost/cost.go
index 3c7fbfc..8657d32 100644
--- a/cost/cost.go
+++ b/cost/cost.go
@@ -1,6 +1,8 @@
 package cost
 
 import (
+	"fmt"
+
 	"github.com/shopspring/decimal"
 )
 
@@ -12,19 +14,21 @@ var HoursPerMonth = decimal.NewFromInt(730)
 type Cost struct {
 	// Decimal is price per month.
 	decimal.Decimal
+	// Currency of the cost.
+	Currency string
 }
 
 // Zero is Cost with zero value.
 var Zero = Cost{}
 
-// NewMonthly returns a new Cost from price per month.
-func NewMonthly(monthly decimal.Decimal) Cost {
-	return Cost{Decimal: monthly}
+// NewMonthly returns a new Cost from price per month with currency.
+func NewMonthly(monthly decimal.Decimal, currency string) Cost {
+	return Cost{Decimal: monthly, Currency: currency}
 }
 
-// NewHourly returns a new Cost from price per hour.
-func NewHourly(hourly decimal.Decimal) Cost {
-	return Cost{Decimal: hourly.Mul(HoursPerMonth)}
+// NewHourly returns a new Cost from price per hour with currency.
+func NewHourly(hourly decimal.Decimal, currency string) Cost {
+	return Cost{Decimal: hourly.Mul(HoursPerMonth), Currency: currency}
 }
 
 // Monthly returns the cost per month.
@@ -38,11 +42,21 @@ func (c Cost) Hourly() decimal.Decimal {
 }
 
 // Add adds the values of two Cost structs.
-func (c Cost) Add(c2 Cost) Cost {
-	return Cost{Decimal: c.Decimal.Add(c2.Monthly())}
+// If the currency of both costs doesn't match, error is returned.
+func (c Cost) Add(c2 Cost) (Cost, error) {
+	// If there is no currency, use the currency of the addition
+	if c.Currency == "" {
+		c.Currency = c2.Currency
+	}
+
+	if c.Currency != c2.Currency {
+		return Zero, fmt.Errorf("currency mismatch: expected %s, got %s", c.Currency, c2.Currency)
+	}
+
+	return Cost{Decimal: c.Decimal.Add(c2.Monthly()), Currency: c.Currency}, nil
 }
 
 // MulDecimal multiplies the Cost by the given decimal.Decimal.
 func (c Cost) MulDecimal(d decimal.Decimal) Cost {
-	return Cost{Decimal: c.Decimal.Mul(d)}
+	return Cost{Decimal: c.Decimal.Mul(d), Currency: c.Currency}
 }
diff --git a/cost/plan.go b/cost/plan.go
index 036abc5..b4e801a 100644
--- a/cost/plan.go
+++ b/cost/plan.go
@@ -18,17 +18,17 @@ func NewPlan(prior, planned *State) *Plan {
 }
 
 // PriorCost returns the total cost of the Prior State or decimal.Zero if it isn't included in the plan.
-func (p Plan) PriorCost() Cost {
+func (p Plan) PriorCost() (Cost, error) {
 	if p.Prior == nil {
-		return Zero
+		return Zero, nil
 	}
 	return p.Prior.Cost()
 }
 
 // PlannedCost returns the total cost of the Planned State or decimal.Zero if it isn't included in the plan.
-func (p Plan) PlannedCost() Cost {
+func (p Plan) PlannedCost() (Cost, error) {
 	if p.Planned == nil {
-		return Zero
+		return Zero, nil
 	}
 	return p.Planned.Cost()
 }
diff --git a/cost/resource.go b/cost/resource.go
index 8e571de..7d5065f 100644
--- a/cost/resource.go
+++ b/cost/resource.go
@@ -1,5 +1,7 @@
 package cost
 
+import "fmt"
+
 // Resource represents costs of a single cloud resource. Each Resource includes a Component map, keyed
 // by the label.
 type Resource struct {
@@ -10,12 +12,17 @@ type Resource struct {
 }
 
 // Cost returns the sum of costs of every Component of this Resource.
-func (re Resource) Cost() Cost {
+// Error is returned if there is a mismatch in Component currency.
+func (re Resource) Cost() (Cost, error) {
 	var total Cost
-	for _, comp := range re.Components {
-		total = total.Add(comp.Cost())
+	var err error
+	for name, comp := range re.Components {
+		total, err = total.Add(comp.Cost())
+		if err != nil {
+			return Zero, fmt.Errorf("failed to add cost of component %s: %w", name, err)
+		}
 	}
-	return total
+	return total, nil
 }
 
 // ResourceDiff is the difference in costs between prior and planned Resource. It contains a ComponentDiff
@@ -28,21 +35,31 @@ type ResourceDiff struct {
 }
 
 // PriorCost returns the sum of costs of every Component's PriorCost.
-func (rd ResourceDiff) PriorCost() Cost {
+// Error is returned if there is a mismatch between currencies of the Components.
+func (rd ResourceDiff) PriorCost() (Cost, error) {
 	total := Zero
+	var err error
 	for _, cd := range rd.ComponentDiffs {
-		total = total.Add(cd.PriorCost())
+		total, err = total.Add(cd.PriorCost())
+		if err != nil {
+			return Zero, fmt.Errorf("failed calculating prior cost : %w", err)
+		}
 	}
-	return total
+	return total, nil
 }
 
 // PlannedCost returns the sum of costs of every Component's PlannedCost.
-func (rd ResourceDiff) PlannedCost() Cost {
+// Error is returned if there is a mismatch between currencies of the Components.
+func (rd ResourceDiff) PlannedCost() (Cost, error) {
 	total := Zero
+	var err error
 	for _, cd := range rd.ComponentDiffs {
-		total = total.Add(cd.PlannedCost())
+		total, err = total.Add(cd.PriorCost())
+		if err != nil {
+			return Zero, fmt.Errorf("failed calculating planned cost: %w", err)
+		}
 	}
-	return total
+	return total, nil
 }
 
 // Errors returns a map of Component errors keyed by the Component label.
diff --git a/cost/state.go b/cost/state.go
index f1c6703..033ab76 100644
--- a/cost/state.go
+++ b/cost/state.go
@@ -58,11 +58,11 @@ func NewState(ctx context.Context, backend Backend, queries []query.Resource) (*
 			}
 
 			quantity := comp.MonthlyQuantity
-			rate := NewMonthly(prices[0].Value)
+			rate := NewMonthly(prices[0].Value, prices[0].Currency)
 
 			if quantity.IsZero() {
 				quantity = comp.HourlyQuantity
-				rate = NewHourly(prices[0].Value)
+				rate = NewHourly(prices[0].Value, prices[0].Currency)
 			}
 
 			component := Component{
@@ -80,12 +80,21 @@ func NewState(ctx context.Context, backend Backend, queries []query.Resource) (*
 }
 
 // Cost returns the sum of the costs of every Resource included in this State.
-func (s *State) Cost() Cost {
+// Error is returned if there is a mismatch in resource currencies.
+func (s *State) Cost() (Cost, error) {
 	var total Cost
-	for _, re := range s.Resources {
-		total = total.Add(re.Cost())
+	for name, re := range s.Resources {
+		rCost, err := re.Cost()
+		if err != nil {
+			return Zero, fmt.Errorf("failed to get cost of resource %s: %w", name, err)
+		}
+		total, err = total.Add(rCost)
+		if err != nil {
+			return Zero, fmt.Errorf("failed to add cost of resource %s: %w", name, err)
+		}
 	}
-	return total
+
+	return total, nil
 }
 
 // ensureResource creates Resource at the given address if it doesn't already exist.
diff --git a/go.mod b/go.mod
index 9e72455..e6de98c 100644
--- a/go.mod
+++ b/go.mod
@@ -17,4 +17,5 @@ require (
 	github.com/spf13/afero v1.6.0
 	github.com/stretchr/testify v1.6.1
 	github.com/zclconf/go-cty v1.8.2
+	golang.org/x/text v0.3.6 // indirect
 )
diff --git a/go.sum b/go.sum
index d8916a5..d605d9c 100644
--- a/go.sum
+++ b/go.sum
@@ -630,6 +630,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/mysql/price.go b/mysql/price.go
index b2cddd6..077eee7 100644
--- a/mysql/price.go
+++ b/mysql/price.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"strings"
 
+	"golang.org/x/text/currency"
+
 	"github.com/cycloidio/sqlr"
 	"github.com/shopspring/decimal"
 
@@ -52,10 +54,15 @@ func newPrice(pwp *price.WithProduct) (*dbPrice, error) {
 		return nil, err
 	}
 
+	cur, err := currency.ParseISO(pwp.Currency)
+	if err != nil {
+		return nil, err
+	}
+
 	return &dbPrice{
 		ProductID:  pwp.Product.ID,
 		Hash:       pwp.GenerateHash(),
-		Currency:   pwp.Currency,
+		Currency:   cur.String(),
 		Value:      pwp.Value,
 		Unit:       pwp.Unit,
 		Attributes: string(attributes),


Test patch:
diff --git a/cost/component_test.go b/cost/component_test.go
index 49790ce..6e68a55 100644
--- a/cost/component_test.go
+++ b/cost/component_test.go
@@ -18,7 +18,7 @@ func TestComponentDiff_PriorCost(t *testing.T) {
 	})
 
 	t.Run("WithValue", func(t *testing.T) {
-		cd := cost.ComponentDiff{Prior: &cost.Component{Quantity: decimal.NewFromInt(5), Rate: cost.NewMonthly(decimal.NewFromFloat(1.5))}}
+		cd := cost.ComponentDiff{Prior: &cost.Component{Quantity: decimal.NewFromInt(5), Rate: cost.NewMonthly(decimal.NewFromFloat(1.5), "USD")}}
 		actual := cd.PriorCost()
 		assert.True(t, actual.Equal(decimal.NewFromFloat(7.5)))
 	})
@@ -32,7 +32,7 @@ func TestComponentDiff_PlannedCost(t *testing.T) {
 	})
 
 	t.Run("WithValue", func(t *testing.T) {
-		cd := cost.ComponentDiff{Planned: &cost.Component{Quantity: decimal.NewFromInt(5), Rate: cost.NewMonthly(decimal.NewFromFloat(1.5))}}
+		cd := cost.ComponentDiff{Planned: &cost.Component{Quantity: decimal.NewFromInt(5), Rate: cost.NewMonthly(decimal.NewFromFloat(1.5), "USD")}}
 		actual := cd.PlannedCost()
 		assert.True(t, actual.Equal(decimal.NewFromFloat(7.5)))
 	})
diff --git a/cost/cost_test.go b/cost/cost_test.go
index 1e35365..df088f4 100644
--- a/cost/cost_test.go
+++ b/cost/cost_test.go
@@ -10,36 +10,46 @@ import (
 
 func TestNewHourly(t *testing.T) {
 	val := decimal.NewFromFloat(1.23)
-	c := cost.NewHourly(val)
+	c := cost.NewHourly(val, "USD")
 	assertDecimalEqual(t, val.Mul(cost.HoursPerMonth), c.Decimal)
 }
 
 func TestNewMonthly(t *testing.T) {
 	val := decimal.NewFromFloat(1.23)
-	c := cost.NewMonthly(val)
+	c := cost.NewMonthly(val, "USD")
 	assertDecimalEqual(t, val, c.Decimal)
 }
 
 func TestCost_Hourly(t *testing.T) {
 	val := decimal.NewFromFloat(1.23)
-	c := cost.NewMonthly(val.Mul(cost.HoursPerMonth))
+	c := cost.NewMonthly(val.Mul(cost.HoursPerMonth), "USD")
 	assertDecimalEqual(t, val, c.Hourly())
 }
 
 func TestCost_Monthly(t *testing.T) {
 	val := decimal.NewFromFloat(1.23)
-	c := cost.NewHourly(val)
+	c := cost.NewHourly(val, "USD")
 	assertDecimalEqual(t, val.Mul(cost.HoursPerMonth), c.Monthly())
 }
 
 func TestCost_Add(t *testing.T) {
-	c1 := cost.NewMonthly(decimal.NewFromFloat(1.23))
-	c2 := cost.NewMonthly(decimal.NewFromFloat(3.21))
-	assertDecimalEqual(t, decimal.NewFromFloat(4.44), c1.Add(c2).Decimal)
+	t.Run("Success", func(t *testing.T) {
+		c1 := cost.NewMonthly(decimal.NewFromFloat(1.23), "USD")
+		c2 := cost.NewMonthly(decimal.NewFromFloat(3.21), "USD")
+		ac, err := c1.Add(c2)
+		assert.NoError(t, err)
+		assertDecimalEqual(t, decimal.NewFromFloat(4.44), ac.Decimal)
+	})
+	t.Run("CurrencyMismatch", func(t *testing.T) {
+		c1 := cost.NewMonthly(decimal.NewFromFloat(1.23), "USD")
+		c2 := cost.NewMonthly(decimal.NewFromFloat(3.21), "EUR")
+		_, err := c1.Add(c2)
+		assert.EqualError(t, err, "currency mismatch: expected USD, got EUR")
+	})
 }
 
 func TestCost_MulDecimal(t *testing.T) {
-	c := cost.NewMonthly(decimal.NewFromFloat(1.23))
+	c := cost.NewMonthly(decimal.NewFromFloat(1.23), "USD")
 	d := decimal.NewFromInt(3)
 	assertDecimalEqual(t, decimal.NewFromFloat(3.69), c.MulDecimal(d).Decimal)
 }
diff --git a/cost/plan_test.go b/cost/plan_test.go
index 1ddf401..49b3e3b 100644
--- a/cost/plan_test.go
+++ b/cost/plan_test.go
@@ -19,7 +19,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 						},
 					},
 				},
@@ -36,7 +36,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 					Prior: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 					},
 				},
 			},
@@ -51,7 +51,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 						},
 					},
 				},
@@ -68,7 +68,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 					Planned: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 					},
 				},
 			},
@@ -83,7 +83,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.50), "USD"),
 						},
 					},
 				},
@@ -92,7 +92,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 						},
 					},
 				},
@@ -105,7 +105,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 						},
 					},
 				},
@@ -114,7 +114,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(3.21)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(3.21), "USD"),
 						},
 					},
 				},
@@ -131,12 +131,12 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 					Prior: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.50)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.50), "USD"),
 					},
 					Planned: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 					},
 				},
 			},
@@ -148,7 +148,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 					Planned: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(3.21)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(3.21), "USD"),
 					},
 				},
 			},
@@ -160,7 +160,7 @@ func TestPlan_ResourceDifferences(t *testing.T) {
 					Prior: &cost.Component{
 						Quantity: decimal.NewFromInt(730),
 						Unit:     "Hrs",
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
+						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
 					},
 				},
 			},
@@ -177,7 +177,7 @@ func TestPlan_SkippedAddresses(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 						},
 					},
 				},
@@ -200,7 +200,7 @@ func TestPlan_SkippedAddresses(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 						},
 					},
 				},
@@ -223,7 +223,7 @@ func TestPlan_SkippedAddresses(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 						},
 					},
 				},
@@ -238,7 +238,7 @@ func TestPlan_SkippedAddresses(t *testing.T) {
 						"EC2 instance hours": {
 							Quantity: decimal.NewFromInt(730),
 							Unit:     "Hrs",
-							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50)),
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(2.50), "USD"),
 						},
 					},
 				},
diff --git a/cost/state_test.go b/cost/state_test.go
index 0b04b56..ad6b3ce 100644
--- a/cost/state_test.go
+++ b/cost/state_test.go
@@ -57,7 +57,7 @@ func TestNewState(t *testing.T) {
 
 		prod1 := &product.Product{ID: product.ID(1)}
 		productRepo.EXPECT().Filter(ctx, queries[0].Components[0].ProductFilter).Return([]*product.Product{prod1}, nil)
-		prc1 := &price.Price{Value: decimal.NewFromFloat(1.23), Unit: "Hrs"}
+		prc1 := &price.Price{Value: decimal.NewFromFloat(1.23), Unit: "Hrs", Currency: "USD"}
 		priceRepo.EXPECT().Filter(ctx, prod1.ID, queries[0].Components[0].PriceFilter).Return([]*price.Price{prc1}, nil)
 
 		expected := &cost.State{
@@ -65,7 +65,7 @@ func TestNewState(t *testing.T) {
 				"aws_instance.test1": {
 					Components: map[string]cost.Component{
 						"Compute": {
-							Rate:     cost.NewMonthly(decimal.New(89790, -2)),
+							Rate:     cost.NewMonthly(decimal.New(89790, -2), "USD"),
 							Quantity: decimal.NewFromInt(1),
 						},
 					},
@@ -121,19 +121,45 @@ func TestNewState(t *testing.T) {
 }
 
 func TestState_Cost(t *testing.T) {
-	state := &cost.State{
-		Resources: map[string]cost.Resource{
-			"aws_instance.test1": {
-				Components: map[string]cost.Component{
-					"Compute": {
-						Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23)),
-						Quantity: decimal.NewFromInt(730),
+	t.Run("Success", func(t *testing.T) {
+		state := &cost.State{
+			Resources: map[string]cost.Resource{
+				"aws_instance.test1": {
+					Components: map[string]cost.Component{
+						"Compute": {
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
+							Quantity: decimal.NewFromInt(730),
+						},
 					},
 				},
 			},
-		},
-	}
+		}
+
+		expected := cost.NewMonthly(decimal.New(89790, -2), "USD")
+		actual, err := state.Cost()
+		assert.NoError(t, err)
+		assert.Equal(t, expected, actual)
+	})
+	t.Run("ComponentCostMismatch", func(t *testing.T) {
+		state := &cost.State{
+			Resources: map[string]cost.Resource{
+				"aws_instance.test1": {
+					Components: map[string]cost.Component{
+						"Compute": {
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "USD"),
+							Quantity: decimal.NewFromInt(730),
+						},
+						"Storage": {
+							Rate:     cost.NewMonthly(decimal.NewFromFloat(1.23), "EUR"),
+							Quantity: decimal.NewFromInt(730),
+						},
+					},
+				},
+			},
+		}
 
-	expected := decimal.NewFromFloat(897.9)
-	assert.True(t, expected.Equal(state.Cost().Monthly()))
+		actual, err := state.Cost()
+		assert.Error(t, err)
+		assert.Equal(t, cost.Zero, actual)
+	})
 }
diff --git a/e2e/aws_estimation_test.go b/e2e/aws_estimation_test.go
index 699c263..75122cc 100644
--- a/e2e/aws_estimation_test.go
+++ b/e2e/aws_estimation_test.go
@@ -135,8 +135,13 @@ func TestAWSEstimation(t *testing.T) {
 		plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformProviderInitializer)
 		require.NoError(t, err)
 
-		assertDecimalEqual(t, decimal.NewFromFloat(91.2), plan.PriorCost().Monthly())
-		assertDecimalEqual(t, decimal.NewFromFloat(901.5), plan.PlannedCost().Monthly())
+		pcost, err := plan.PriorCost()
+		assert.NoError(t, err)
+		assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), pcost)
+
+		pcost, err = plan.PlannedCost()
+		assert.NoError(t, err)
+		assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), pcost)
 
 		diffs := plan.ResourceDifferences()
 		require.Len(t, diffs, 2)
@@ -148,19 +153,19 @@ func TestAWSEstimation(t *testing.T) {
 				require.NotNil(t, compute)
 				assert.Equal(t, []string{"Linux", "on-demand", "t2.micro"}, compute.Prior.Details)
 				assert.Equal(t, []string{"Linux", "on-demand", "t2.xlarge"}, compute.Planned.Details)
-				assertDecimalEqual(t, decimal.NewFromFloat(87.6), compute.PriorCost().Monthly())
-				assertDecimalEqual(t, decimal.NewFromFloat(897.9), compute.PlannedCost().Monthly())
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(87.6), "USD"), compute.PriorCost())
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(897.9), "USD"), compute.PlannedCost())
 
 				rootVol := diff.ComponentDiffs["Root volume: Storage"]
 				require.NotNil(t, rootVol)
-				assertDecimalEqual(t, decimal.NewFromFloat(3.6), rootVol.PriorCost().Monthly())
-				assertDecimalEqual(t, decimal.NewFromFloat(3.6), rootVol.PlannedCost().Monthly())
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PriorCost())
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PlannedCost())
 
 			case "aws_lb.example":
 				lb := diff.ComponentDiffs["Application Load Balancer"]
 				require.NotNil(t, lb)
 				assert.False(t, diff.Valid())
-				assertDecimalEqual(t, decimal.NewFromFloat(0), lb.Planned.Cost().Monthly())
+				assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), lb.Planned.Cost())
 			}
 		}
 	})
@@ -179,8 +184,8 @@ func TestAWSEstimation(t *testing.T) {
 
 		rootVol := diffs[0].ComponentDiffs["Root volume: Storage"]
 		require.NotNil(t, rootVol)
-		assertDecimalEqual(t, decimal.NewFromFloat(3.6), rootVol.PriorCost().Monthly())
-		assertDecimalEqual(t, decimal.NewFromFloat(3.6), rootVol.PlannedCost().Monthly())
+		assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PriorCost())
+		assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PlannedCost())
 
 		expected := map[string]error{
 			"Compute": cost.ErrProductNotFound,
@@ -189,6 +194,7 @@ func TestAWSEstimation(t *testing.T) {
 	})
 }
 
-func assertDecimalEqual(t *testing.T, expected, actual decimal.Decimal) {
-	assert.Truef(t, expected.Equal(actual), "Not equal:\nexpected: %s\nactual  : %s", expected, actual)
+func assertCostEqual(t *testing.T, expected, actual cost.Cost) {
+	assert.Truef(t, expected.Equal(actual.Decimal), "Not equal:\nexpected value: %s\nactual value: %s", expected, actual)
+	assert.Truef(t, expected.Currency == actual.Currency, "Not equal:\nexpected currency: %s\nactual currency: %s", expected.Currency, actual.Currency)
 }