88using VirtoCommerce . CatalogModule . Data . Repositories ;
99using VirtoCommerce . CoreModule . Core . Seo ;
1010using VirtoCommerce . Platform . Core . Common ;
11+ using VirtoCommerce . StoreModule . Core . Services ;
1112
1213namespace VirtoCommerce . CatalogModule . Data . Services ;
1314
@@ -16,17 +17,20 @@ public class CatalogSeoResolver : ISeoResolver
1617 private readonly Func < ICatalogRepository > _repositoryFactory ;
1718 private readonly ICategoryService _categoryService ;
1819 private readonly IItemService _itemService ;
20+ private readonly IStoreService _storeService ;
1921
2022 private const string CategoryObjectType = "Category" ;
2123 private const string CatalogProductObjectType = "CatalogProduct" ;
2224
2325 public CatalogSeoResolver ( Func < ICatalogRepository > repositoryFactory ,
2426 ICategoryService categoryService ,
25- IItemService itemService )
27+ IItemService itemService ,
28+ IStoreService storeService )
2629 {
2730 _repositoryFactory = repositoryFactory ;
2831 _categoryService = categoryService ;
2932 _itemService = itemService ;
33+ _storeService = storeService ;
3034 }
3135
3236 public async Task < IList < SeoInfo > > FindSeoAsync ( SeoSearchCriteria criteria )
@@ -44,12 +48,15 @@ public async Task<IList<SeoInfo>> FindSeoAsync(SeoSearchCriteria criteria)
4448
4549 if ( currentEntitySeoInfos . Count == 0 )
4650 {
51+ return [ ] ;
52+
53+ // TODO: Uncomment this block of code when frontend will support deactivated seo entries and redirect it to real seo
4754 // Try to find deactivated seo entries and revert it back if we found it
48- currentEntitySeoInfos = await SearchSeoInfos ( criteria . StoreId , criteria . LanguageCode , segments . Last ( ) , false ) ;
49- if ( currentEntitySeoInfos . Count == 0 )
50- {
51- return [ ] ;
52- }
55+ // currentEntitySeoInfos = await SearchSeoInfos(criteria.StoreId, criteria.LanguageCode, segments.Last(), false);
56+ // if (currentEntitySeoInfos.Count == 0)
57+ // {
58+ // return [];
59+ // }
5360 }
5461
5562 var groups = currentEntitySeoInfos . GroupBy ( x => new { x . ObjectType , x . ObjectId } ) ;
@@ -60,74 +67,114 @@ public async Task<IList<SeoInfo>> FindSeoAsync(SeoSearchCriteria criteria)
6067 return [ currentEntitySeoInfos . First ( ) ] ;
6168 }
6269
70+ var parentIds = new List < string > ( ) ;
71+
72+ var store = await _storeService . GetByIdAsync ( criteria . StoreId ) ;
73+
6374 // It's not possibe to resolve because we don't have parent segment
6475 if ( segments . Length == 1 )
6576 {
66- return [ ] ;
77+ parentIds . Add ( store . Catalog ) ;
6778 }
79+ else
80+ {
81+ // We found multiple seo information by seo search criteria, need to find correct by checking parent.
82+ var parentSearchCriteria = criteria . Clone ( ) as SeoSearchCriteria ;
83+ parentSearchCriteria . Permalink = string . Join ( '/' , segments . Take ( segments . Length - 1 ) ) ;
84+ var parentSeoInfos = await FindSeoAsync ( parentSearchCriteria ) ;
6885
69- // We found multiple seo information by seo search criteria, need to find correct by checking parent.
70- var parentSearchCriteria = criteria . Clone ( ) as SeoSearchCriteria ;
71- parentSearchCriteria . Permalink = string . Join ( '/' , segments . Take ( segments . Length - 1 ) ) ;
72- var parentSeoInfos = await FindSeoAsync ( parentSearchCriteria ) ;
86+ if ( parentSeoInfos . Count == 0 )
87+ {
88+ return [ ] ;
89+ }
7390
74- if ( parentSeoInfos . Count == 0 )
75- {
76- return [ ] ;
91+ parentIds . AddRange ( parentSeoInfos . Select ( x => x . ObjectId ) . Distinct ( ) ) ;
7792 }
7893
79- var parentCategorieIds = parentSeoInfos . Select ( x => x . ObjectId ) . Distinct ( ) . ToList ( ) ;
80-
8194 foreach ( var groupKey in groups . Select ( g => g . Key ) )
8295 {
83- if ( groupKey . ObjectType == CategoryObjectType )
96+ if ( groupKey . ObjectType . Equals ( CategoryObjectType , StringComparison . OrdinalIgnoreCase ) )
8497 {
85- var isMatch = await DoesParentMatchCategoryOutline ( parentCategorieIds , groupKey . ObjectId ) ;
98+ var isMatch = await DoesParentMatchCategoryOutline ( store . Catalog , parentIds , groupKey . ObjectId ) ;
8699 if ( isMatch )
87100 {
88101 return currentEntitySeoInfos . Where ( x =>
89- x . ObjectId == groupKey . ObjectId
90- && groupKey . ObjectType == CategoryObjectType ) . ToList ( ) ;
102+ groupKey . ObjectId . Equals ( x . ObjectId , StringComparison . OrdinalIgnoreCase )
103+ && groupKey . ObjectType . Equals ( CategoryObjectType , StringComparison . OrdinalIgnoreCase ) ) . ToList ( ) ;
91104 }
92105 }
93106
94107 // Inside the method
95- else if ( groupKey . ObjectType == CatalogProductObjectType )
108+ else if ( groupKey . ObjectType . Equals ( CatalogProductObjectType , StringComparison . OrdinalIgnoreCase ) )
96109 {
97- var isMatch = await DoesParentMatchProductOutline ( parentCategorieIds , groupKey . ObjectId ) ;
110+ var isMatch = await DoesParentMatchProductOutline ( store . Catalog , parentIds , groupKey . ObjectId ) ;
98111
99112 if ( isMatch )
100113 {
101114 return currentEntitySeoInfos . Where ( x =>
102- x . ObjectId == groupKey . ObjectId
103- && groupKey . ObjectType == CatalogProductObjectType ) . ToList ( ) ;
115+ groupKey . ObjectId . Equals ( x . ObjectId , StringComparison . OrdinalIgnoreCase )
116+ && groupKey . ObjectType . Equals ( CatalogProductObjectType , StringComparison . OrdinalIgnoreCase ) ) . ToList ( ) ;
104117 }
105118 }
106119 }
107120
108121 return [ ] ;
109122 }
110123
111- private async Task < bool > DoesParentMatchCategoryOutline ( IList < string > parentCategorieIds , string objectId )
124+ private async Task < bool > DoesParentMatchCategoryOutline ( string catalogId , IList < string > parentCategorieIds , string objectId )
112125 {
113126 var category = await _categoryService . GetByIdAsync ( objectId , CategoryResponseGroup . WithOutlines . ToString ( ) , false ) ;
114127 if ( category == null )
115128 {
116129 throw new InvalidOperationException ( $ "Category with ID '{ objectId } ' was not found.") ;
117130 }
118- var outlines = category . Outlines . Select ( x => x . Items . Skip ( x . Items . Count - 2 ) . First ( ) . Id ) . Distinct ( ) . ToList ( ) ;
119- return outlines . Any ( parentCategorieIds . Contains ) ;
131+
132+ if ( category . Outlines . Count == 0 )
133+ {
134+ return false ;
135+ }
136+
137+ // Select outline for current catalog and longest path to find real parent
138+ var maxLength = category . Outlines
139+ . Where ( x => string . Equals ( x . Items . FirstOrDefault ( ) ? . Id , catalogId , StringComparison . OrdinalIgnoreCase ) )
140+ . Select ( x => x . Items . Count )
141+ . DefaultIfEmpty ( 0 )
142+ . Max ( ) ;
143+
144+ // Get parent from longest path. Keep in mind that latest element is current object id.
145+ var categoryParents = category . Outlines
146+ . Where ( x => x . Items . Count == maxLength )
147+ . SelectMany ( x => x . Items . Skip ( x . Items . Count - 2 ) . Take ( 1 ) . Select ( i => i . Id ) )
148+ . Distinct ( )
149+ . ToList ( ) ;
150+
151+ return categoryParents . Any ( parentCategorieIds . Contains ) ;
120152 }
121153
122- private async Task < bool > DoesParentMatchProductOutline ( IList < string > parentCategorieIds , string objectId )
154+ private async Task < bool > DoesParentMatchProductOutline ( string catalogId , IList < string > parentCategorieIds , string objectId )
123155 {
124156 var product = await _itemService . GetByIdAsync ( objectId , CategoryResponseGroup . WithOutlines . ToString ( ) , false ) ;
125157 if ( product == null )
126158 {
127159 throw new InvalidOperationException ( $ "Product with ID '{ objectId } ' was not found.") ;
128160 }
129- var outlines = product . Outlines . Select ( x => x . Items . Skip ( x . Items . Count - 2 ) . First ( ) . Id ) . Distinct ( ) . ToList ( ) ;
130- return outlines . Any ( parentCategorieIds . Contains ) ;
161+
162+ // Select outline for current catalog and longest path to find real parent
163+ var maxLength = product . Outlines
164+ . Where ( x => string . Equals ( x . Items . FirstOrDefault ( ) ? . Id , catalogId , StringComparison . OrdinalIgnoreCase ) )
165+ . Select ( x => x . Items . Count )
166+ . Max ( ) ;
167+
168+ // Get parent from longest path. Keep in mind that latest element is current product id.
169+ var categoryParents = product . Outlines
170+ . Where ( x => x . Items . Count == maxLength )
171+ . SelectMany ( x => x . Items . Select ( x => x . Id )
172+ . Skip ( x . Items . Count - 2 )
173+ . Take ( 1 ) )
174+ . Distinct ( )
175+ . ToList ( ) ;
176+
177+ return categoryParents . Any ( parentCategorieIds . Contains ) ;
131178 }
132179
133180 private async Task < List < SeoInfo > > SearchSeoInfos ( string storeId , string languageCode , string slug , bool isActive = true )
@@ -136,8 +183,8 @@ private async Task<List<SeoInfo>> SearchSeoInfos(string storeId, string language
136183
137184 return ( await repository . SeoInfos . Where ( s => s . IsActive == isActive
138185 && s . Keyword == slug
139- && ( s . StoreId == null || s . StoreId == storeId )
140- && ( s . Language == null || s . Language == languageCode ) )
186+ && ( string . IsNullOrEmpty ( s . StoreId ) || s . StoreId == storeId )
187+ && ( string . IsNullOrEmpty ( s . Language ) || s . Language == languageCode ) )
141188 . ToListAsync ( ) )
142189 . Select ( x => x . ToModel ( AbstractTypeFactory < SeoInfo > . TryCreateInstance ( ) ) )
143190 . OrderByDescending ( s => GetPriorityScore ( s , storeId , languageCode ) )
@@ -150,12 +197,12 @@ private static int GetPriorityScore(SeoInfo seoInfo, string storeId, string lang
150197 var hasStoreCriteria = ! string . IsNullOrEmpty ( storeId ) ;
151198 var hasLangCriteria = ! string . IsNullOrEmpty ( language ) ;
152199
153- if ( hasStoreCriteria && seoInfo . StoreId == storeId )
200+ if ( hasStoreCriteria && string . Equals ( seoInfo . StoreId , storeId , StringComparison . OrdinalIgnoreCase ) )
154201 {
155202 score += 2 ;
156203 }
157204
158- if ( hasLangCriteria && seoInfo . LanguageCode == language )
205+ if ( hasLangCriteria && string . Equals ( seoInfo . LanguageCode , language , StringComparison . OrdinalIgnoreCase ) )
159206 {
160207 score += 1 ;
161208 }
0 commit comments