diff --git a/broker/adapter/api_directory.go b/broker/adapter/api_directory.go index c3d6973e..78894dc4 100644 --- a/broker/adapter/api_directory.go +++ b/broker/adapter/api_directory.go @@ -176,11 +176,11 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl Match: false, }) } - priority := math.MaxInt + sharedPriority := math.MaxInt for name, reqNet := range reqNetworks { if _, ok := supNetworks[name]; ok { - if priority > reqNet.Priority { - priority = reqNet.Priority + if sharedPriority > reqNet.Priority { + sharedPriority = reqNet.Priority } for i, n := range supMatch.Networks { if n.Name == name { @@ -202,11 +202,12 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl } return cmp.Compare(a.Name, b.Name) }) - if priority < math.MaxInt { - sup.Priority = priority + if sharedPriority < math.MaxInt { + sup.Priority = sharedPriority suppTiers := getPeerTiers(sup.CustomData) supMatch.Tiers = make([]TierMatch, 0, len(suppTiers)) cost := math.MaxFloat64 + priority := math.MaxInt for _, suppTier := range suppTiers { var tierMatch TierMatch tierMatch.Name = suppTier.Name @@ -217,8 +218,10 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl suppTypeMatch := svcType == "" || svcType == strings.ToLower(suppTier.Type) suppLevelMatch := svcLevel == "" || svcLevel == strings.ToLower(suppTier.Level) suppCostMatch := costMatches(suppTier.Cost, maxCost) + tierPriority := sharedNetworkPriorityForCost(suppTier.Cost, reqNetworks, supNetworks) + suppNetworkMatch := tierPriority < math.MaxInt - if suppTypeMatch && suppLevelMatch && suppCostMatch { + if suppTypeMatch && suppLevelMatch && suppCostMatch && suppNetworkMatch { reciprocal := true //supplier tier matched the request, if the tier is free it must be reciprocal if suppTier.Cost == 0 { @@ -234,8 +237,9 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl } } tierMatch.Match = reciprocal - if reciprocal && cost > suppTier.Cost { + if reciprocal && (cost > suppTier.Cost || cost == suppTier.Cost && priority > tierPriority) { cost = suppTier.Cost + priority = tierPriority } } supMatch.Tiers = append(supMatch.Tiers, tierMatch) @@ -257,6 +261,7 @@ func (a *ApiDirectory) FilterAndSort(ctx common.ExtendedContext, entries []Suppl supMatch.Match = true supMatch.Cost = fmt.Sprintf("%.2f", cost) sup.Cost = cost + sup.Priority = priority filtered = append(filtered, sup) } supMatch.Priority = sup.Priority @@ -289,6 +294,32 @@ func costMatches(suppCost, maxCost float64) bool { } } +func sharedNetworkPriorityForCost(suppCost float64, reqNetworks, supNetworks map[string]Network) int { + priority := math.MaxInt + for name, reqNet := range reqNetworks { + supNet, ok := supNetworks[name] + if !ok { + continue + } + if networkAllowsCost(reqNet, suppCost) && networkAllowsCost(supNet, suppCost) { + if priority > reqNet.Priority { + priority = reqNet.Priority + } + } + } + return priority +} + +func networkAllowsCost(network Network, cost float64) bool { + if network.Reciprocal == nil { + return true + } + if *network.Reciprocal { + return cost == 0 + } + return cost > 0 +} + func CompareSuppliers(a, b SupplierOrdering) int { if a.IsLocal() && !b.IsLocal() { return -1 @@ -316,8 +347,9 @@ func getPeerNetworks(peerData directory.Entry) map[string]Network { for _, n := range *peerData.Networks { if n.Priority != nil { networks[n.Name] = Network{ - Name: n.Name, - Priority: int(*n.Priority), + Name: n.Name, + Priority: int(*n.Priority), + Reciprocal: n.Reciprocal, } } } diff --git a/broker/adapter/directory.go b/broker/adapter/directory.go index 77630de6..5c69963a 100644 --- a/broker/adapter/directory.go +++ b/broker/adapter/directory.go @@ -59,8 +59,9 @@ func (s Supplier) IsLocal() bool { return s.Local } func (s Supplier) GetRatio() float32 { return s.Ratio } type Network struct { - Name string `json:"name"` - Priority int `json:"priority"` + Name string `json:"name"` + Priority int `json:"priority"` + Reciprocal *bool `json:"reciprocal,omitempty"` } type Tier struct { diff --git a/broker/test/adapter/api_directory_test.go b/broker/test/adapter/api_directory_test.go index 09e795f2..64941b6d 100644 --- a/broker/test/adapter/api_directory_test.go +++ b/broker/test/adapter/api_directory_test.go @@ -33,6 +33,26 @@ func createDirectoryAdapter(urls ...string) adapter.DirectoryLookupAdapter { return adapter.CreateApiDirectory(http.DefaultClient, urls) } +func boolPtr(v bool) *bool { + return &v +} + +func intPtr(v int) *int { + return &v +} + +func withNetworkReciprocal(entry directory.Entry, reciprocal *bool) directory.Entry { + if entry.Networks == nil { + return entry + } + networks := slices.Clone(*entry.Networks) + for i := range networks { + networks[i].Reciprocal = reciprocal + } + entry.Networks = &networks + return entry +} + func TestGetVendorFromUrl(t *testing.T) { tests := []struct { name string @@ -495,6 +515,143 @@ func TestFilterAndSortReciprocal(t *testing.T) { assert.Equal(t, "copy", rotaInfo.Request.Type) } +func TestFilterAndSortReciprocalNetworkExcludesPaidTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[0], boolPtr(true)) + entries := []adapter.Supplier{ + {PeerId: "2", Ratio: 0.7, Symbol: "AU-NU", CustomData: dirEntries.Items[2]}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Empty(t, entries) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.False(t, rotaInfo.Suppliers[0].Match) +} + +func TestFilterAndSortPaidNetworkExcludesFreeTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[4], boolPtr(false)) + entries := []adapter.Supplier{ + {PeerId: "3", Ratio: 0.7, Symbol: "AU-VVWA", CustomData: dirEntries.Items[4]}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Rush", + }, + ServiceType: iso18626.TypeServiceTypeCopy, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 0, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Empty(t, entries) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.False(t, rotaInfo.Suppliers[0].Match) +} + +func TestFilterAndSortPaidNetworkAllowsPaidTiers(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterData := withNetworkReciprocal(dirEntries.Items[0], boolPtr(false)) + entries := []adapter.Supplier{ + {PeerId: "2", Ratio: 0.7, Symbol: "AU-NU", CustomData: withNetworkReciprocal(dirEntries.Items[2], boolPtr(false))}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, rotaInfo := ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Len(t, entries, 1) + assert.Equal(t, "2", entries[0].PeerId) + assert.Equal(t, 34.4, entries[0].Cost) + assert.Len(t, rotaInfo.Suppliers, 1) + assert.True(t, rotaInfo.Suppliers[0].Match) + assert.Equal(t, "34.40", rotaInfo.Suppliers[0].Cost) +} + +func TestFilterAndSortUsesCompatibleNetworkPriority(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + ad := createDirectoryAdapter("") + requesterNetworks := []directory.Network{ + {Name: "Reciprocal", Priority: intPtr(1), Reciprocal: boolPtr(true)}, + {Name: "Paid Low", Priority: intPtr(5), Reciprocal: boolPtr(false)}, + {Name: "Paid High", Priority: intPtr(3), Reciprocal: boolPtr(false)}, + } + paidTier := []directory.Tier{ + {Name: "Paid Core Loan", Level: "Core", Type: "Loan", Cost: 34.4}, + } + requesterData := directory.Entry{Name: "Requester", Networks: &requesterNetworks} + supplierANetworks := []directory.Network{ + {Name: "Reciprocal", Priority: intPtr(1), Reciprocal: boolPtr(true)}, + {Name: "Paid Low", Priority: intPtr(5), Reciprocal: boolPtr(false)}, + } + supplierBNetworks := []directory.Network{ + {Name: "Paid High", Priority: intPtr(3), Reciprocal: boolPtr(false)}, + } + entries := []adapter.Supplier{ + {PeerId: "A", Symbol: "A", CustomData: directory.Entry{Name: "Supplier A", Networks: &supplierANetworks, Tiers: &paidTier}}, + {PeerId: "B", Symbol: "B", CustomData: directory.Entry{Name: "Supplier B", Networks: &supplierBNetworks, Tiers: &paidTier}}, + } + serviceInfo := iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Core", + }, + ServiceType: iso18626.TypeServiceTypeLoan, + } + billingInfo := iso18626.BillingInfo{ + MaximumCosts: &iso18626.TypeCosts{ + MonetaryValue: utils.XSDDecimal{ + Base: 3500, + Exp: 2, + }, + }, + } + + entries, _ = ad.FilterAndSort(appCtx, entries, requesterData, &serviceInfo, &billingInfo) + + assert.Len(t, entries, 2) + assert.Equal(t, "B", entries[0].PeerId) + assert.Equal(t, 3, entries[0].Priority) + assert.Equal(t, "A", entries[1].PeerId) + assert.Equal(t, 5, entries[1].Priority) +} + func TestFilterAndSortNoFilters(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) ad := createDirectoryAdapter("") diff --git a/directory/directory_api.yaml b/directory/directory_api.yaml index 59b59bc4..90de3a23 100644 --- a/directory/directory_api.yaml +++ b/directory/directory_api.yaml @@ -144,6 +144,8 @@ components: type: string priority: type: integer + reciprocal: + type: boolean Membership: type: object required: