Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions internal/archera/recommendation_alternatives.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ type CandidateIdentity struct {
SelectedAmount *big.Rat
}

// CandidateStatus classifies a vendor candidate against the stored recommendation.
type CandidateStatus string

const (
CandidateVendorAlternative CandidateStatus = "vendor_alternative"
CandidateUnavailable CandidateStatus = "unavailable"
)

type CandidateAssociation struct {
OfferID, Status, Reason, NativeIdentityStatus string
OfferID string
Status CandidateStatus
Reason Reason
NativeIdentityStatus CandidateStatus
}

func associateCandidate(r *RecommendationIdentity, c *CandidateIdentity) CandidateAssociation {
out := CandidateAssociation{OfferID: c.OfferID, Status: "unavailable", Reason: "candidate_basis_unproven", NativeIdentityStatus: "unavailable"}
if validMatchUUID(c.OfferID) && c.Provider == r.Provider && c.Type == "savings_plan" && validPayment(c.Payment) && sameAmount(r.HourlyCommitment, c.SelectedAmount) {
out.Status, out.Reason = "vendor_alternative", ""
out := CandidateAssociation{OfferID: c.OfferID, Status: CandidateUnavailable, Reason: ReasonCandidateBasisUnproven, NativeIdentityStatus: CandidateUnavailable}
if validMatchUUID(c.OfferID) && c.Provider == r.Provider && c.Type == commitmentSavingsPlan && validPayment(c.Payment) && sameAmount(r.HourlyCommitment, c.SelectedAmount) {
out.Status, out.Reason = CandidateVendorAlternative, ""
}
return out
}
10 changes: 5 additions & 5 deletions internal/archera/recommendation_alternatives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ func TestMatchRecommendationPreservesVendorAlternativesWithoutNativeClaims(t *te
require.Len(t, got.Candidates, 3)
for i, association := range got.Candidates {
require.Equal(t, rows[0].Candidates[i].OfferID, association.OfferID)
require.Equal(t, "vendor_alternative", association.Status)
require.Equal(t, CandidateVendorAlternative, association.Status)
require.Empty(t, association.Reason)
require.Equal(t, "unavailable", association.NativeIdentityStatus)
require.Equal(t, CandidateUnavailable, association.NativeIdentityStatus)
}
require.Equal(t, "unavailable_source_precision", got.EconomicStatus)
require.Equal(t, ReasonUnavailableSourcePrecision, got.EconomicStatus)
after, err := json.Marshal([]any{r, rows, items})
require.NoError(t, err)
require.Equal(t, before, after)
Expand Down Expand Up @@ -64,9 +64,9 @@ func TestMatchRecommendationDoesNotBorrowCandidateBasis(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ResourceAssociated, got.Status)
require.Len(t, got.Candidates, 1)
require.Equal(t, "unavailable", got.Candidates[0].Status)
require.Equal(t, CandidateUnavailable, got.Candidates[0].Status)
require.NotEmpty(t, got.Candidates[0].Reason)
require.Equal(t, "unavailable", got.Candidates[0].NativeIdentityStatus)
require.Equal(t, CandidateUnavailable, got.Candidates[0].NativeIdentityStatus)
})
}
}
Expand Down
69 changes: 69 additions & 0 deletions internal/archera/recommendation_contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package archera

import (
"errors"

"github.com/LeanerCloud/cloud-commitments-go/pkg/insurance"
)

// ProjectContract projects the published comparison and line items into the identities MatchRecommendation joins.
func ProjectContract(c *insurance.Comparison, items []insurance.LineItem) ([]ComparisonIdentity, []LineItemIdentity, error) {
if c == nil {
return nil, nil, errors.New("archera comparison identity is unavailable")
}
rows := make([]ComparisonIdentity, 0, len(c.Rows))
for i := range c.Rows {
r := &c.Rows[i]
current := contractCandidate(&r.Current)
rows = append(rows, ComparisonIdentity{Region: r.Current.Region, IsCurrent: r.Current.IsCurrent,
LineItemID: r.LineItemID, CurrentOfferID: current.OfferID, Provider: current.Provider,
CurrentType: current.Type, CurrentPayment: current.Payment, CurrentSelectedAmount: current.SelectedAmount,
Candidates: vendorAlternatives(r.Candidates)})
}
identities := make([]LineItemIdentity, 0, len(items))
for i := range items {
identities = append(identities, contractLineItem(&items[i]))
}
return rows, identities, nil
}

// vendorAlternatives drops the entry the vendor flags as the current offer, which is never an alternative.
func vendorAlternatives(entries []insurance.OfferEntry) []CandidateIdentity {
var out []CandidateIdentity
for i := range entries {
if !entries[i].IsCurrent {
out = append(out, contractCandidate(&entries[i]))
}
}
return out
}

func contractLineItem(li *insurance.LineItem) LineItemIdentity {
offer := &li.Offer
kind := offer.Type
if !li.IsSelected || (offer.PaymentOption != nil && *offer.PaymentOption != li.PaymentOption) {
kind = ""
}
return LineItemIdentity{
ID: li.ID, PlanID: li.PlanID, OfferID: li.OfferID, Provider: string(offer.Provider),
SourceAccountID: identityText(li.AccountID), ProviderOfferingID: offer.ProviderOfferingID, Type: kind,
PlanType: identityText(offer.PlanType), Region: identityText(offer.Region), InstanceFamily: identityText(offer.InstanceFamily),
Payment: string(li.PaymentOption), DurationSeconds: offer.DurationSeconds, SelectedCommitment: li.SelectedCommitment,
}
}

func contractCandidate(offer *insurance.OfferEntry) CandidateIdentity {
out := CandidateIdentity{OfferID: offer.OfferID, Provider: string(offer.Provider),
Type: offer.CommitmentType, SelectedAmount: offer.SelectedAmount}
if offer.PaymentOption != nil {
out.Payment = string(*offer.PaymentOption)
}
return out
}

func identityText(value *string) string {
if value == nil {
return ""
}
return *value
}
155 changes: 155 additions & 0 deletions internal/archera/recommendation_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package archera

import (
"math/big"
"strings"
"testing"
"time"

"github.com/LeanerCloud/cloud-commitments-go/pkg/insurance"
"github.com/stretchr/testify/require"
)

func contractMatchFixture(t *testing.T) (*insurance.Comparison, []insurance.LineItem) {
t.Helper()
item := `{"id":"` + matchItem + `","plan_id":"` + matchPlan + `","offer_id":"` + matchOffer + `","account_id":"Source-account-a","selected_quantity":7,"selected_commitment":0.50000000000000000001,"is_selected":true,"contract_term":"one_year_gris","payment_option":"No Upfront","contract_spec":{"commitment_type":"opaque-contract-type"},"offer":{"id":"` + matchOffer + `","type":"savings_plan","provider":"aws","provider_offering_id":"provider-offer-a","offering_id":"distinct-catalog-offer","duration_seconds":31536000,"region":"us-east-1","az":null,"instance_type":null,"instance_family":"m5","product_description":null,"tenancy":null,"plan_type":"EC2Instance","is_multi_az":null,"is_flexible":null,"payment_option":"No Upfront"}}`
items, err := insurance.DecodeLineItems(strings.NewReader("["+item+"]"), matchPlan)
require.NoError(t, err)
current := `{"is_current":true,"offer_id":"` + matchOffer + `","selected_amount":0.50000000000000000001,"commitment_type":"savings_plan","contract_term":"one_year_gris","payment_option":"no_upfront","offer":{"provider":"aws","region":"us-east-1"},"discount_rate":0,"commitment_upfront_cost":0,"commitment_financials_monthly_rate":{},"delta_vs_current":{"monthly_net_savings":0,"upfront_cost":0,"discount_rate":0}}`
candidate := strings.ReplaceAll(strings.Replace(current, `"is_current":true`, `"is_current":false`, 1), matchOffer, "faadf00d-ffff-4aba-8ada-abcdefabcdef")
candidate = strings.Replace(candidate, "no_upfront", "all_upfront", 1)
body := `{"current_totals":{"commitment_financials_monthly_rate":{},"commitment_upfront_cost":0},"hypothetical_totals":[],"data":[{"line_item_id":"` + matchItem + `","current":` + current + `,"candidates":[` + candidate + "," + candidate + `]}]}`
comparison, err := insurance.DecodeComparison(strings.NewReader(body), "fixture-org", matchPlan, time.Unix(1, 0))
require.NoError(t, err)
return comparison, items
}

func TestProjectContractJoinsActualDecodedIdentity(t *testing.T) {
comparison, items := contractMatchFixture(t)
record := storedMatchFixture()
rec, err := ProjectRecommendation(&record)
require.NoError(t, err)
rows, identities, err := ProjectContract(comparison, items)
require.NoError(t, err)
require.Len(t, rows[0].Candidates, 2)
require.Equal(t, "all_upfront", rows[0].Candidates[0].Payment)
require.Equal(t, rows[0].Candidates[0].OfferID, rows[0].Candidates[1].OfferID)
require.Equal(t, "provider-offer-a", identities[0].ProviderOfferingID)
require.Equal(t, "opaque-contract-type", items[0].ContractSpec.CommitmentType)
require.Equal(t, "one_year_gris", items[0].ContractTerm)
got, err := MatchRecommendation(matchPlan, rec, rows, identities)
require.NoError(t, err)
require.Equal(t, ResourceAssociated, got.Status)
require.Len(t, got.Candidates, 2)
require.Equal(t, CandidateVendorAlternative, got.Candidates[0].Status)
require.Equal(t, ReasonStoredDecimalOnly, got.AmountProvenance)
require.Equal(t, ReasonUnavailableSourcePrecision, got.EconomicStatus)
_, _, err = ProjectContract(nil, items)
require.Error(t, err)
}

func TestProjectContractPreservesMissingOrContradictoryEvidence(t *testing.T) {
tests := []struct {
name string
change func(*insurance.Comparison, *insurance.LineItem)
}{
{"account", func(_ *insurance.Comparison, i *insurance.LineItem) { i.AccountID = nil }},
{"catalog_is_not_provider_offering", func(_ *insurance.Comparison, i *insurance.LineItem) { i.Offer.ProviderOfferingID = "" }},
{"duration", func(_ *insurance.Comparison, i *insurance.LineItem) { i.Offer.DurationSeconds = nil }},
{"region", func(_ *insurance.Comparison, i *insurance.LineItem) { i.Offer.Region = nil }},
{"family", func(_ *insurance.Comparison, i *insurance.LineItem) { i.Offer.InstanceFamily = nil }},
{"plan_type", func(_ *insurance.Comparison, i *insurance.LineItem) { i.Offer.PlanType = nil }},
{"unselected", func(_ *insurance.Comparison, i *insurance.LineItem) { i.IsSelected = false }},
{"payment", func(_ *insurance.Comparison, i *insurance.LineItem) {
p := insurance.PaymentAllUpfront
i.Offer.PaymentOption = &p
}},
{"current_flag", func(c *insurance.Comparison, _ *insurance.LineItem) { c.Rows[0].Current.IsCurrent = false }},
{"current_region", func(c *insurance.Comparison, _ *insurance.LineItem) { c.Rows[0].Current.Region = nil }},
{"current_type", func(c *insurance.Comparison, _ *insurance.LineItem) { c.Rows[0].Current.CommitmentType = "ri" }},
{"amount", func(c *insurance.Comparison, _ *insurance.LineItem) {
c.Rows[0].Current.SelectedAmount = big.NewRat(1, 2)
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, items := contractMatchFixture(t)
tt.change(c, &items[0])
record := storedMatchFixture()
rec, err := ProjectRecommendation(&record)
require.NoError(t, err)
rows, identities, err := ProjectContract(c, items)
require.NoError(t, err)
got, err := MatchRecommendation(matchPlan, rec, rows, identities)
require.NoError(t, err)
require.Equal(t, MatchUnavailable, got.Status)
})
}
}

func TestProjectContractExcludesTheCurrentOfferFromAlternatives(t *testing.T) {
comparison, items := contractMatchFixture(t)
row := &comparison.Rows[0]
current := row.Current
current.IsCurrent = true
row.Candidates = []insurance.OfferEntry{row.Candidates[0], current, row.Candidates[1]}
rows, _, err := ProjectContract(comparison, items)
require.NoError(t, err)
require.Len(t, rows[0].Candidates, 2)
for _, candidate := range rows[0].Candidates {
require.NotEqual(t, current.OfferID, candidate.OfferID)
}
record := storedMatchFixture()
rec, err := ProjectRecommendation(&record)
require.NoError(t, err)
_, identities, err := ProjectContract(comparison, items)
require.NoError(t, err)
got, err := MatchRecommendation(matchPlan, rec, rows, identities)
require.NoError(t, err)
require.Len(t, got.Candidates, 2)
require.Empty(t, vendorAlternatives(nil))
}

func TestVendorAlternativesMapsEveryField(t *testing.T) {
payment := insurance.PaymentPartialUpfront
entries := []insurance.OfferEntry{
{IsCurrent: true, OfferID: "current"},
{OfferID: "offer-a", Provider: "aws", CommitmentType: "savings_plan", PaymentOption: &payment, SelectedAmount: big.NewRat(3, 4)},
{OfferID: "offer-b", Provider: "azure", CommitmentType: "ri"},
}
got := vendorAlternatives(entries)
require.Equal(t, []CandidateIdentity{
{OfferID: "offer-a", Provider: "aws", Type: "savings_plan", Payment: "partial_upfront", SelectedAmount: big.NewRat(3, 4)},
{OfferID: "offer-b", Provider: "azure", Type: "ri"},
}, got)
}

func TestProjectContractCarriesPaymentOptions(t *testing.T) {
comparison, items := contractMatchFixture(t)
upfront, partial := insurance.PaymentAllUpfront, insurance.PaymentPartialUpfront
items[0].PaymentOption, items[0].Offer.PaymentOption = upfront, &upfront
comparison.Rows[0].Current.PaymentOption = &partial
rows, identities, err := ProjectContract(comparison, items)
require.NoError(t, err)
require.Equal(t, "all_upfront", identities[0].Payment)
require.Equal(t, "partial_upfront", rows[0].CurrentPayment)
}

func TestProjectContractUnselectedTwinDoesNotCreateAmbiguity(t *testing.T) {
comparison, items := contractMatchFixture(t)
twin := items[0]
twin.ID = "eaadf00d-ffff-4aba-8ada-abcdefabcdef"
twin.IsSelected = false
items = append(items, twin)
record := storedMatchFixture()
rec, err := ProjectRecommendation(&record)
require.NoError(t, err)
_, identities, err := ProjectContract(comparison, items)
require.NoError(t, err)
require.Empty(t, identities[1].Type)
id, status := SelectLineItem(rec, identities)
require.Equal(t, SelectOne, status)
require.Equal(t, matchItem, id)
_, status = SelectLineItem(rec, identities[1:])
require.Equal(t, SelectNone, status)
}
Loading
Loading