diff --git a/pkg/insurance/amounts.go b/pkg/insurance/amounts.go new file mode 100644 index 0000000..10adc51 --- /dev/null +++ b/pkg/insurance/amounts.go @@ -0,0 +1,17 @@ +package insurance + +import ( + "errors" + "math/big" +) + +func nonnegativeRat(n wireNum) (*big.Rat, error) { + r, err := requiredRat(n) + if err != nil { + return nil, err + } + if r.Sign() < 0 { + return nil, errors.New("must be nonnegative") + } + return r, nil +} diff --git a/pkg/insurance/quote.go b/pkg/insurance/quote.go index f0e2e9c..4a5f32b 100644 --- a/pkg/insurance/quote.go +++ b/pkg/insurance/quote.go @@ -217,12 +217,11 @@ func (w wireHypothetical) toHypothetical() (Hypothetical, error) { } // wireOfferEntry mirrors the consumed subset of OfferComparisonEntry. -// selected_amount and offer_org_id feed the vendor's update operation and are -// deliberately not decoded. type wireOfferEntry struct { - IsCurrent *bool `json:"is_current"` - OfferID string `json:"offer_id"` - Offer *struct { + IsCurrent *bool `json:"is_current"` + OfferID string `json:"offer_id"` + SelectedAmount wireNum `json:"selected_amount"` + Offer *struct { Provider *string `json:"provider"` Region *string `json:"region"` GuaranteedDisplayName *string `json:"guaranteed_display_name"` @@ -273,11 +272,15 @@ func (w *wireOfferEntry) toOfferEntry() (OfferEntry, error) { } e.IsCurrent = *w.IsCurrent e.OfferID = w.OfferID + amount, err := nonnegativeRat(w.SelectedAmount) + if err != nil { + return e, fmt.Errorf("selected_amount: %w", err) + } + e.SelectedAmount = amount e.CommitmentType = w.CommitmentType e.LeaseMenuItemID = w.LeaseMenuItemID e.GuaranteedDisplayName = w.Offer.GuaranteedDisplayName e.Region = w.Offer.Region - var err error if e.Provider, err = parseProvider(w.Offer.Provider); err != nil { return e, fmt.Errorf("offer.provider: %w", err) } diff --git a/pkg/insurance/quote_test.go b/pkg/insurance/quote_test.go index f07e9ea..05b9009 100644 --- a/pkg/insurance/quote_test.go +++ b/pkg/insurance/quote_test.go @@ -122,6 +122,7 @@ func TestDecodeComparison_Golden(t *testing.T) { row := c.Rows[0] assert.Equal(t, "22222222-2222-4222-8222-222222222222", row.LineItemID) assert.True(t, row.Current.IsCurrent) + assert.Equal(t, rat(t, "3"), row.Current.SelectedAmount) assert.False(t, row.Current.LeaseBacked()) assert.Nil(t, row.Current.ContractTerm) assert.Nil(t, row.Current.PaymentOption) @@ -141,6 +142,19 @@ func TestDecodeComparison_Golden(t *testing.T) { assert.Nil(t, cand.Delta.BreakevenDays) } +func TestDecodeComparisonSelectedAmount(t *testing.T) { + t.Parallel() + for _, value := range []string{"null", "-1", `"3"`} { + _, err := decodeComparison(t, strings.ReplaceAll(validComparisonJSON(), `"selected_amount": 3`, `"selected_amount": `+value)) + require.ErrorContains(t, err, "selected_amount") + } + _, err := decodeComparison(t, strings.ReplaceAll(validComparisonJSON(), `"selected_amount": 3,`, ``)) + require.ErrorContains(t, err, "selected_amount") + c, err := decodeComparison(t, strings.ReplaceAll(validComparisonJSON(), `"selected_amount": 3`, `"selected_amount": 0.1234567890123456789`)) + require.NoError(t, err) + assert.Equal(t, rat(t, "0.1234567890123456789"), c.Rows[0].Current.SelectedAmount) +} + func TestDecodeComparison_EmptyArraysAreValid(t *testing.T) { c, err := decodeComparison(t, `{ "current_totals": {"commitment_financials_monthly_rate": {}, "commitment_upfront_cost": 0}, @@ -428,6 +442,7 @@ func TestDecodeComparison_DistinctValuesForEveryMappedField(t *testing.T) { "line_item_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", "current": { "is_current": true, "offer_id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "selected_amount": 3.25, "offer": {"provider": "gcp", "region": "europe-west4", "guaranteed_display_name": "Current G"}, "lease_menu_item_id": null, "commitment_type": "gcp/cud", "contract_term": "three_year", "payment_option": "all_upfront", @@ -437,6 +452,7 @@ func TestDecodeComparison_DistinctValuesForEveryMappedField(t *testing.T) { }, "candidates": [{ "is_current": false, "offer_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "selected_amount": 4.125, "offer": {"provider": "azure", "region": "eu-west-1", "guaranteed_display_name": "Cand G"}, "lease_menu_item_id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", "commitment_type": "azure/Virtual Machines", "contract_term": "one_year_gris", "payment_option": "partial_upfront", @@ -480,6 +496,7 @@ func TestDecodeComparison_DistinctValuesForEveryMappedField(t *testing.T) { cur := row.Current assert.True(t, cur.IsCurrent) assert.Equal(t, "cccccccc-cccc-4ccc-8ccc-cccccccccccc", cur.OfferID) + assert.Equal(t, rat(t, "3.25"), cur.SelectedAmount) assert.Equal(t, "gcp/cud", cur.CommitmentType) assert.Equal(t, common.ProviderGCP, cur.Provider) require.NotNil(t, cur.Region) @@ -497,6 +514,7 @@ func TestDecodeComparison_DistinctValuesForEveryMappedField(t *testing.T) { cand := row.Candidates[0] assert.False(t, cand.IsCurrent) assert.Equal(t, "dddddddd-dddd-4ddd-8ddd-dddddddddddd", cand.OfferID) + assert.Equal(t, rat(t, "4.125"), cand.SelectedAmount) assert.Equal(t, "azure/Virtual Machines", cand.CommitmentType) assert.Equal(t, common.ProviderAzure, cand.Provider) require.NotNil(t, cand.Region) diff --git a/pkg/insurance/types.go b/pkg/insurance/types.go index 7a4c498..69ccf99 100644 --- a/pkg/insurance/types.go +++ b/pkg/insurance/types.go @@ -135,14 +135,14 @@ type OfferDelta struct { } // OfferEntry mirrors the consumed subset of OfferComparisonEntry: one offer -// (current or candidate) for a line item. Fields that feed the vendor's -// line-item update operation (selected_amount, offer_org_id) are deliberately -// not mapped. +// (current or candidate) for a line item. type OfferEntry struct { // IsCurrent is true when this entry matches the line item's current // offer and lease. IsCurrent bool OfferID string + // SelectedAmount is a count for RI/unit CUD, or $/hour for spend commitments. + SelectedAmount *big.Rat // CommitmentType is the vendor's offer type string. The docs give both // "aws/AmazonEC2"-style and "ri"/"savings_plan"/"cud"-style examples; // treat it as opaque display text.