From c6408f09f9ed07d2f22dcbc935062a6bd8bc1621 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 17 Oct 2026 17:17:37 +0200 Subject: [PATCH 1/3] feat(archera): select the matching plan line item and type match statuses SelectLineItem exports the line-item selection MatchRecommendation already did, so the handler can pick the item before a scoped comparison call. ProjectContract now projects the published insurance line items and comparison into the match identities, and vendorAlternatives drops the candidate the vendor flags as current. Match, candidate, amount and economic statuses become typed constants (CandidateStatus, Reason), and validPayment uses the insurance.Payment constants. Refs #799 Co-Authored-By: Claude Opus 5.5 --- .../archera/recommendation_alternatives.go | 19 ++- .../recommendation_alternatives_test.go | 10 +- internal/archera/recommendation_contract.go | 69 +++++++++ .../archera/recommendation_contract_test.go | 135 ++++++++++++++++++ internal/archera/recommendation_match.go | 87 ++++++++--- internal/archera/recommendation_match_test.go | 81 ++++++++++- 6 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 internal/archera/recommendation_contract.go create mode 100644 internal/archera/recommendation_contract_test.go diff --git a/internal/archera/recommendation_alternatives.go b/internal/archera/recommendation_alternatives.go index 0a4cfee6c..b1da28b38 100644 --- a/internal/archera/recommendation_alternatives.go +++ b/internal/archera/recommendation_alternatives.go @@ -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 } diff --git a/internal/archera/recommendation_alternatives_test.go b/internal/archera/recommendation_alternatives_test.go index 4f8a41554..cb1b991ca 100644 --- a/internal/archera/recommendation_alternatives_test.go +++ b/internal/archera/recommendation_alternatives_test.go @@ -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) @@ -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) }) } } diff --git a/internal/archera/recommendation_contract.go b/internal/archera/recommendation_contract.go new file mode 100644 index 000000000..0549f7cc7 --- /dev/null +++ b/internal/archera/recommendation_contract.go @@ -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 +} diff --git a/internal/archera/recommendation_contract_test.go b/internal/archera/recommendation_contract_test.go new file mode 100644 index 000000000..8b949ef6e --- /dev/null +++ b/internal/archera/recommendation_contract_test.go @@ -0,0 +1,135 @@ +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 }}, + {"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) +} diff --git a/internal/archera/recommendation_match.go b/internal/archera/recommendation_match.go index a8d06e6ae..948fe02c1 100644 --- a/internal/archera/recommendation_match.go +++ b/internal/archera/recommendation_match.go @@ -6,6 +6,7 @@ import ( "strings" "unicode" + "github.com/LeanerCloud/cloud-commitments-go/pkg/insurance" "github.com/google/uuid" ) @@ -43,14 +44,29 @@ const ( ResourceAssociated MatchStatus = "resource_associated" ) +// Reason names why a match, candidate, amount or economic status holds. +type Reason string + +const ( + ReasonIdentityUnproven Reason = "identity_unproven" + ReasonAmbiguousLineItems Reason = "ambiguous_line_items" + ReasonComparisonBasisUnproven Reason = "comparison_basis_unproven" + ReasonCandidateBasisUnproven Reason = "candidate_basis_unproven" + ReasonStoredDecimalOnly Reason = "stored_decimal_only" + ReasonUnavailableSourcePrecision Reason = "unavailable_source_precision" +) + +const commitmentSavingsPlan = "savings_plan" + type RecommendationMatch struct { - Status MatchStatus - LineItemID, Reason, AmountProvenance, EconomicStatus string - Candidates []CandidateAssociation + Status MatchStatus + LineItemID string + Reason, AmountProvenance, EconomicStatus Reason + Candidates []CandidateAssociation } func MatchRecommendation(planID string, rec *RecommendationIdentity, rows []ComparisonIdentity, items []LineItemIdentity) (RecommendationMatch, error) { - unavailable := RecommendationMatch{Status: MatchUnavailable, Reason: "identity_unproven"} + unavailable := RecommendationMatch{Status: MatchUnavailable, Reason: ReasonIdentityUnproven} if !validRecommendation(rec) { return unavailable, nil } @@ -61,28 +77,22 @@ func MatchRecommendation(planID string, rec *RecommendationIdentity, rows []Comp if index == nil { return unavailable, nil } - var selected *LineItemIdentity - for i := range items { - if !sameResource(rec, &items[i]) { - continue - } - if selected != nil { - unavailable.Reason = "ambiguous_line_items" - return unavailable, nil - } - selected = &items[i] - } - if selected == nil { + id, status := SelectLineItem(rec, items) + switch status { + case SelectAmbiguous: + unavailable.Reason = ReasonAmbiguousLineItems + return unavailable, nil + case SelectNone: return unavailable, nil } - row := index[strings.ToLower(selected.ID)] + row := index[strings.ToLower(id)] if !sameCurrentBasis(rec, row) { - unavailable.Reason = "comparison_basis_unproven" + unavailable.Reason = ReasonComparisonBasisUnproven return unavailable, nil } result := RecommendationMatch{ Status: ResourceAssociated, LineItemID: row.LineItemID, - AmountProvenance: "stored_decimal_only", EconomicStatus: "unavailable_source_precision", + AmountProvenance: ReasonStoredDecimalOnly, EconomicStatus: ReasonUnavailableSourcePrecision, } for i := range row.Candidates { result.Candidates = append(result.Candidates, associateCandidate(rec, &row.Candidates[i])) @@ -90,6 +100,37 @@ func MatchRecommendation(planID string, rec *RecommendationIdentity, rows []Comp return result, nil } +// SelectStatus says how many line items share the recommendation's resource identity. +type SelectStatus string + +const ( + SelectOne SelectStatus = "selected" + SelectNone SelectStatus = "none" + SelectAmbiguous SelectStatus = "ambiguous" +) + +// SelectLineItem returns the ID of the single line item that is the recommendation's resource, +// or an empty ID with SelectNone (no match, or an unsupported recommendation) or SelectAmbiguous. +func SelectLineItem(rec *RecommendationIdentity, items []LineItemIdentity) (string, SelectStatus) { + if !validRecommendation(rec) { + return "", SelectNone + } + id, found := "", false + for i := range items { + if !sameResource(rec, &items[i]) { + continue + } + if found { + return "", SelectAmbiguous + } + id, found = items[i].ID, true + } + if !found { + return "", SelectNone + } + return id, SelectOne +} + func validRecommendation(r *RecommendationIdentity) bool { if r == nil { return false @@ -113,8 +154,8 @@ func validDimensions(r *RecommendationIdentity) bool { } func validPayment(payment string) bool { - switch payment { - case "no_upfront", "partial_upfront", "all_upfront": + switch insurance.PaymentOption(payment) { + case insurance.PaymentNoUpfront, insurance.PaymentPartialUpfront, insurance.PaymentAllUpfront: return true default: return false @@ -132,13 +173,13 @@ type resourceIdentity struct { func sameResource(r *RecommendationIdentity, item *LineItemIdentity) bool { want := resourceIdentity{r.Provider, r.SourceAccountID, r.OfferingID, r.PlanType, r.Region, r.InstanceFamily, r.Payment} got := resourceIdentity{item.Provider, item.SourceAccountID, item.ProviderOfferingID, item.PlanType, item.Region, item.InstanceFamily, item.Payment} - return want == got && item.Type == "savings_plan" && item.DurationSeconds != nil && + return want == got && item.Type == commitmentSavingsPlan && item.DurationSeconds != nil && item.DurationSeconds.Cmp(new(big.Rat).SetInt64(r.NativeDurationSeconds)) == 0 && sameAmount(r.HourlyCommitment, item.SelectedCommitment) } func sameCurrentBasis(r *RecommendationIdentity, row *ComparisonIdentity) bool { - return row != nil && row.IsCurrent && row.Region != nil && *row.Region == r.Region && row.Provider == r.Provider && row.CurrentType == "savings_plan" && + return row != nil && row.IsCurrent && row.Region != nil && *row.Region == r.Region && row.Provider == r.Provider && row.CurrentType == commitmentSavingsPlan && row.CurrentPayment == r.Payment && sameAmount(r.HourlyCommitment, row.CurrentSelectedAmount) } diff --git a/internal/archera/recommendation_match_test.go b/internal/archera/recommendation_match_test.go index 6da769d74..48a01c217 100644 --- a/internal/archera/recommendation_match_test.go +++ b/internal/archera/recommendation_match_test.go @@ -47,8 +47,8 @@ func TestMatchRecommendationAssociatesOnlyProvenResourceIdentity(t *testing.T) { require.NoError(t, err) require.Equal(t, ResourceAssociated, result.Status) require.Equal(t, matchItem, result.LineItemID) - require.Equal(t, "stored_decimal_only", result.AmountProvenance) - require.Equal(t, "unavailable_source_precision", result.EconomicStatus) + require.Equal(t, ReasonStoredDecimalOnly, result.AmountProvenance) + require.Equal(t, ReasonUnavailableSourcePrecision, result.EconomicStatus) require.Equal(t, "1/2", rec.HourlyCommitment.RatString()) require.Equal(t, "1/2", items[0].SelectedCommitment.RatString()) after, marshalErr := json.Marshal([]any{rec, rows, items}) @@ -201,3 +201,80 @@ func TestMatchRecommendationRejectsEqualMalformedUUIDReferences(t *testing.T) { } } } + +func TestMatchRecommendationUnavailableForNilOrZeroAmounts(t *testing.T) { + zero := func() *big.Rat { return new(big.Rat) } + tests := []struct { + name string + change func(*RecommendationIdentity, *ComparisonIdentity, *LineItemIdentity) + reason Reason + }{ + {"recommendation_nil", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) { r.HourlyCommitment = nil }, ReasonIdentityUnproven}, + {"recommendation_zero", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) { + r.HourlyCommitment = zero() + }, ReasonIdentityUnproven}, + {"item_nil", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { + i.SelectedCommitment = nil + }, ReasonIdentityUnproven}, + {"item_zero", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { + i.SelectedCommitment = zero() + }, ReasonIdentityUnproven}, + {"current_nil", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { + c.CurrentSelectedAmount = nil + }, ReasonComparisonBasisUnproven}, + {"current_zero", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { + c.CurrentSelectedAmount = zero() + }, ReasonComparisonBasisUnproven}, + {"all_zero", func(r *RecommendationIdentity, c *ComparisonIdentity, i *LineItemIdentity) { + r.HourlyCommitment, c.CurrentSelectedAmount, i.SelectedCommitment = zero(), zero(), zero() + }, ReasonIdentityUnproven}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, rows, items := matchFixture() + tt.change(r, &rows[0], &items[0]) + got, err := MatchRecommendation(matchPlan, r, rows, items) + require.NoError(t, err) + require.Equal(t, MatchUnavailable, got.Status) + require.Equal(t, tt.reason, got.Reason) + require.Empty(t, got.LineItemID) + require.Empty(t, got.Candidates) + }) + } +} + +func TestSelectLineItem(t *testing.T) { + r, _, items := matchFixture() + id, status := SelectLineItem(r, items) + require.Equal(t, SelectOne, status) + require.Equal(t, matchItem, id) + + other := items[0] + other.ID = "eaadf00d-ffff-4aba-8ada-abcdefabcdef" + other.Region = "eu-west-1" + id, status = SelectLineItem(r, []LineItemIdentity{other, items[0]}) + require.Equal(t, SelectOne, status) + require.Equal(t, matchItem, id) + + id, status = SelectLineItem(r, []LineItemIdentity{other}) + require.Equal(t, SelectNone, status) + require.Empty(t, id) + id, status = SelectLineItem(r, nil) + require.Equal(t, SelectNone, status) + require.Empty(t, id) + + twin := items[0] + twin.ID = other.ID + id, status = SelectLineItem(r, []LineItemIdentity{items[0], twin}) + require.Equal(t, SelectAmbiguous, status) + require.Empty(t, id) + + bad := *r + bad.HourlyCommitment = nil + id, status = SelectLineItem(&bad, items) + require.Equal(t, SelectNone, status) + require.Empty(t, id) + id, status = SelectLineItem(nil, items) + require.Equal(t, SelectNone, status) + require.Empty(t, id) +} From f9c29e49dda53c130cf8691c4d270d87ead6a920 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 17 Oct 2026 17:22:02 +0200 Subject: [PATCH 2/3] test(archera): keep a non-savings-plan current offer unavailable Kills the mutant that hardcodes CurrentType in ProjectContract. Refs #799 Co-Authored-By: Claude Opus 5.5 --- internal/archera/recommendation_contract_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/archera/recommendation_contract_test.go b/internal/archera/recommendation_contract_test.go index 8b949ef6e..82db7e06a 100644 --- a/internal/archera/recommendation_contract_test.go +++ b/internal/archera/recommendation_contract_test.go @@ -66,6 +66,7 @@ func TestProjectContractPreservesMissingOrContradictoryEvidence(t *testing.T) { }}, {"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) }}, From 7a9721ea507389dbe7d394b21e470d9048f17562 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 17 Oct 2026 17:23:39 +0200 Subject: [PATCH 3/3] test(archera): pin that an unselected twin does not create ambiguity Refs #799 Co-Authored-By: Claude Opus 5.5 --- .../archera/recommendation_contract_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/archera/recommendation_contract_test.go b/internal/archera/recommendation_contract_test.go index 82db7e06a..679ff15c0 100644 --- a/internal/archera/recommendation_contract_test.go +++ b/internal/archera/recommendation_contract_test.go @@ -134,3 +134,22 @@ func TestProjectContractCarriesPaymentOptions(t *testing.T) { 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) +}