Skip to content

Commit 9f22f12

Browse files
authored
VCST-1399: Fixed slug resolving for category/category (#771)
1 parent 989577e commit 9f22f12

File tree

7 files changed

+302
-171
lines changed

7 files changed

+302
-171
lines changed

samples/VirtoCommerce.CatalogModule2.Web/Module.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ public void Initialize(IServiceCollection serviceCollection)
6969

7070
serviceCollection.AddTransient<IProductAssociationSearchService, ProductAssociationSearchService2>();
7171

72-
serviceCollection.AddTransient<ISeoBySlugResolver, CatalogSeoBySlugResolver2>();
73-
7472
serviceCollection.AddTransient<IInternalListEntrySearchService, InternalListEntrySearchService2>();
7573

7674
serviceCollection.AddTransient<ISeoDuplicatesDetector, CatalogSeoDuplicatesDetector2>();

samples/VirtoCommerce.CatalogModule2.Web/Services/CatalogSeoBySlugResolver.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/VirtoCommerce.CatalogModule.Data/Services/CatalogSeoBySlugResolver.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace VirtoCommerce.CatalogModule.Data.Services
1414
{
15+
[Obsolete("Use CatalogSeoResolver", DiagnosticId = "VC0010", UrlFormat = "https://docs.virtocommerce.org/platform/user-guide/versions/virto3-products-versions/")]
1516
public class CatalogSeoBySlugResolver : ISeoBySlugResolver
1617
{
1718
private readonly IPlatformMemoryCache _platformMemoryCache;
@@ -24,11 +25,10 @@ public CatalogSeoBySlugResolver(Func<ICatalogRepository> repositoryFactory, IPla
2425
}
2526

2627
#region ISeoBySlugResolver members
27-
public async Task<SeoInfo[]> FindSeoBySlugAsync(string slug)
28+
public Task<SeoInfo[]> FindSeoBySlugAsync(string slug)
2829
{
29-
3030
var cacheKey = CacheKey.With(GetType(), nameof(FindSeoBySlugAsync), slug);
31-
return await _platformMemoryCache.GetOrCreateExclusiveAsync(cacheKey, async cacheEntry =>
31+
return _platformMemoryCache.GetOrCreateExclusiveAsync(cacheKey, async cacheEntry =>
3232
{
3333
cacheEntry.AddExpirationToken(SeoInfoCacheRegion.CreateChangeToken());
3434
var result = new List<SeoInfo>();

src/VirtoCommerce.CatalogModule.Data/Services/CatalogSeoResolver.cs

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using VirtoCommerce.CatalogModule.Data.Repositories;
99
using VirtoCommerce.CoreModule.Core.Seo;
1010
using VirtoCommerce.Platform.Core.Common;
11+
using VirtoCommerce.StoreModule.Core.Services;
1112

1213
namespace 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
}

src/VirtoCommerce.CatalogModule.Web/Module.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ public void Initialize(IServiceCollection serviceCollection)
154154
serviceCollection.AddTransient<TrackSpecialChangesEventHandler>();
155155

156156
serviceCollection.AddTransient<ISeoResolver, CatalogSeoResolver>();
157+
#pragma warning disable VC0010 // Type or member is obsolete
157158
serviceCollection.AddTransient<ISeoBySlugResolver, CatalogSeoBySlugResolver>();
159+
#pragma warning restore VC0010 // Type or member is obsolete
158160

159161
serviceCollection.AddTransient<IInternalListEntrySearchService, InternalListEntrySearchService>();
160162
serviceCollection.AddTransient<ILinkSearchService, LinkSearchService>();

tests/VirtoCommerce.CatalogModule.Tests/CatalogHierarchyHelper.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@
1010
using VirtoCommerce.CatalogModule.Data.Services;
1111
using VirtoCommerce.CoreModule.Core.Outlines;
1212
using VirtoCommerce.CoreModule.Core.Seo;
13+
using VirtoCommerce.StoreModule.Core.Model;
14+
using VirtoCommerce.StoreModule.Core.Services;
1315

1416
namespace VirtoCommerce.CatalogModule.Tests
1517
{
1618
public class CatalogHierarchyHelper
1719
{
20+
private readonly string _catalogId;
1821
public List<CatalogProduct> Products { get; private set; }
1922
public List<Category> Categories { get; private set; }
2023
public List<SeoInfo> SeoInfos { get; private set; }
2124

22-
public CatalogHierarchyHelper()
25+
public CatalogHierarchyHelper(string catalogId)
2326
{
24-
Products = new List<CatalogProduct>();
25-
Categories = new List<Category>();
26-
SeoInfos = new List<SeoInfo>();
27+
_catalogId = catalogId;
28+
29+
Products = [];
30+
Categories = [];
31+
SeoInfos = [];
2732
}
2833

2934
public void AddProduct(string productId, params string[] outlineIds)
@@ -33,7 +38,7 @@ public void AddProduct(string productId, params string[] outlineIds)
3338
Id = productId,
3439
Outlines = outlineIds.Select(id => new Outline
3540
{
36-
Items = id.Split('/').Select(outlineId => new OutlineItem { Id = outlineId }).ToList()
41+
Items = id.Split('/').Append(productId).Select(outlineId => new OutlineItem { Id = outlineId }).ToList()
3742
}).ToList()
3843
};
3944
Products.Add(product);
@@ -46,9 +51,10 @@ public void AddCategory(string categoryId, params string[] outlineIds)
4651
Id = categoryId,
4752
Outlines = outlineIds.Select(id => new Outline
4853
{
49-
Items = id.Split('/').Select(outlineId => new OutlineItem { Id = outlineId }).ToList()
54+
Items = id.Split('/').Append(categoryId).Select(outlineId => new OutlineItem { Id = outlineId }).ToList()
5055
}).ToList()
5156
};
57+
5258
Categories.Add(category);
5359
}
5460

@@ -71,11 +77,13 @@ public CatalogSeoResolver CreateCatalogSeoResolver()
7177
var catalogRepositoryMock = CreateCatalogRepositoryMock();
7278
var categoryServiceMock = CreateCategoryServiceMock();
7379
var productServiceMock = CreateProductServiceMock();
80+
var storeServiceMock = CreateStoreServiceMock();
7481

7582
return new CatalogSeoResolver(
7683
catalogRepositoryMock.Object,
7784
categoryServiceMock.Object,
78-
productServiceMock.Object);
85+
productServiceMock.Object,
86+
storeServiceMock.Object);
7987
}
8088

8189
public Mock<ICategoryService> CreateCategoryServiceMock()
@@ -130,6 +138,21 @@ public Mock<Func<ICatalogRepository>> CreateCatalogRepositoryMock()
130138
repositoryFactoryMock.Setup(f => f()).Returns(repository.Object);
131139
return repositoryFactoryMock;
132140
}
141+
142+
public Mock<IStoreService> CreateStoreServiceMock()
143+
{
144+
var storeService = new Mock<IStoreService>();
145+
146+
storeService.Setup(x =>
147+
x.GetAsync(It.IsAny<IList<string>>(), It.IsAny<string>(), It.IsAny<bool>()))
148+
.ReturnsAsync((IList<string> ids, string responseGroup, bool clone) =>
149+
{
150+
return [new Store { Catalog = _catalogId }];
151+
});
152+
153+
return storeService;
154+
}
133155
}
134156
}
135157

158+

0 commit comments

Comments
 (0)