Skip to content
Draft
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
22 changes: 18 additions & 4 deletions src/Business/Grand.Business.Common/Services/Stores/StoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public virtual async Task<IList<Store>> GetAllStores()
});
}

/// <summary>
/// Gets the first configured store
/// </summary>
/// <returns>Store</returns>
public virtual Task<Store?> GetFirstStore()
{
return _cacheBase.GetAsync(CacheKey.STORES_FIRST_KEY, () => Task.FromResult(
_storeRepository.Table.OrderBy(x => x.DisplayOrder).FirstOrDefault()));
}

/// <summary>
/// Gets a store
/// </summary>
Expand Down Expand Up @@ -109,8 +119,7 @@ public virtual async Task DeleteStore(Store store)
{
ArgumentNullException.ThrowIfNull(store);

var allStores = await GetAllStores();
if (allStores.Count == 1)
if (!_storeRepository.Table.Skip(1).Any())
throw new Exception("You cannot delete the only configured store");

await _storeRepository.DeleteAsync(store);
Expand All @@ -128,8 +137,13 @@ public virtual async Task DeleteStore(Store store)
/// <returns></returns>
public async Task<Store?> GetStoreByHost(string host)
{
var allStores = await GetAllStores();
return allStores.FirstOrDefault(s => s.ContainsHostValue(host));
if (string.IsNullOrEmpty(host))
return null;

var key = string.Format(CacheKey.STORES_BY_HOST_KEY, host.ToLowerInvariant());
return await _cacheBase.GetAsync(key, () => Task.FromResult(
_storeRepository.Table.FirstOrDefault(s =>
s.Domains.Any(domain => domain.HostName.Equals(host, StringComparison.OrdinalIgnoreCase)))));
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public interface IStoreService
/// <returns>Stores</returns>
Task<IList<Store>> GetAllStores();

/// <summary>
/// Gets the first configured store
/// </summary>
/// <returns>Store</returns>
Task<Store?> GetFirstStore();

/// <summary>
/// Gets a store
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ public virtual async Task<int> SendCampaign(Campaign campaign, EmailAccount emai

var builder = new LiquidObjectBuilder(mediator);
var store = await storeService.GetStoreById(campaign.StoreId) ??
(await storeService.GetAllStores()).FirstOrDefault();
await storeService.GetFirstStore();

builder.AddStoreTokens(store, language, emailAccount)
.AddNewsLetterSubscriptionTokens(subscription, store, contextAccessor.StoreContext.CurrentHost);
Expand Down Expand Up @@ -303,7 +303,7 @@ public virtual async Task SendCampaign(Campaign campaign, EmailAccount emailAcco
(await languageService.GetAllLanguages()).FirstOrDefault();

var store = await storeService.GetStoreById(campaign.StoreId) ??
(await storeService.GetAllStores()).FirstOrDefault();
await storeService.GetFirstStore();

var builder = new LiquidObjectBuilder(mediator);
builder.AddStoreTokens(store, language, emailAccount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public MessageProviderService(

protected virtual async Task<Store> GetStore(string storeId)
{
return await _storeService.GetStoreById(storeId) ?? (await _storeService.GetAllStores()).FirstOrDefault();
return await _storeService.GetStoreById(storeId) ?? await _storeService.GetFirstStore();
}

protected virtual async Task<MessageTemplate> GetMessageTemplate(string messageTemplateName, string storeId)
Expand Down Expand Up @@ -1161,7 +1161,7 @@ public virtual async Task<int> SendGiftVoucherMessage(GiftVoucher giftVoucher, O

Store store = null;
if (order != null) store = await _storeService.GetStoreById(order.StoreId);
store ??= (await _storeService.GetAllStores()).FirstOrDefault();
store ??= await _storeService.GetFirstStore();

var language = await EnsureLanguageIsActive(languageId, store?.Id);

Expand Down Expand Up @@ -1843,7 +1843,7 @@ public virtual async Task<int> SendAuctionEndedStoreOwnerMessage(Product product
}
else
{
var store = (await _storeService.GetAllStores()).FirstOrDefault();
var store = await _storeService.GetFirstStore();
var language = await EnsureLanguageIsActive(languageId, store?.Id);
messageTemplate = await GetMessageTemplate("AuctionExpired.StoreOwnerNotification", "");
if (messageTemplate == null)
Expand Down
13 changes: 13 additions & 0 deletions src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ public static partial class CacheKey
/// </remarks>
public static string STORES_BY_ID_KEY => "Grand.stores.id-{0}";

/// <summary>
/// Key for caching
/// </summary>
public static string STORES_FIRST_KEY => "Grand.stores.first";

/// <summary>
/// Key for caching
/// </summary>
/// <remarks>
/// {0} : host
/// </remarks>
public static string STORES_BY_HOST_KEY => "Grand.stores.host-{0}";

#endregion

#region Tax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Grand.Data;
using Grand.Domain.Stores;
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Caching.Constants;
using Grand.Infrastructure.Events;
using MediatR;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -21,6 +22,8 @@ public class StoreServiceTests
public void Init()
{
_cacheMock = new Mock<ICacheBase>();
_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<Store>>>()))
.Returns<string, Func<Task<Store>>>((_, acquire) => acquire());
_mediatorMock = new Mock<IMediator>();
_repository = new Mock<IRepository<Store>>();
_service = new StoreService(_cacheMock.Object, _repository.Object, _mediatorMock.Object);
Expand All @@ -47,8 +50,7 @@ public async Task UpdateStore_ValidArgument_InvokeExpectedMethods()
[TestMethod]
public async Task DeleteStore_ValidArgument_InvokeExpectedMethods()
{
_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<Store>>>>()))
.Returns(Task.FromResult(new List<Store> { new(), new() }));
_repository.Setup(c => c.Table).Returns(new List<Store> { new(), new() }.AsQueryable());
await _service.DeleteStore(new Store());
_repository.Verify(c => c.DeleteAsync(It.IsAny<Store>()), Times.Once);
_mediatorMock.Verify(c => c.Publish(It.IsAny<EntityDeleted<Store>>(), default), Times.Once);
Expand All @@ -59,8 +61,7 @@ public async Task DeleteStore_ValidArgument_InvokeExpectedMethods()
public void DeleteStore_OnlyOneStore_ThrowException()
{
//can not remove store if it is only one
_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<Store>>>>()))
.Returns(Task.FromResult(new List<Store> { new() }));
_repository.Setup(c => c.Table).Returns(new List<Store> { new() }.AsQueryable());
Assert.ThrowsExactlyAsync<Exception>(async () => await _service.DeleteStore(new Store()));
}

Expand All @@ -80,16 +81,15 @@ public async Task GetStoreByHost_MatchingHost_ReturnsMatchingStore()
new DomainHost { HostName = "store2.com", Url = "http://store2.com", Primary = true }
};

var stores = new List<Store> { store1, store2 };

_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<Store>>>>()))
.Returns(Task.FromResult(stores));
_repository.Setup(c => c.Table).Returns(new List<Store> { store1, store2 }.AsQueryable());

// Act
var result = await _service.GetStoreByHost("store1.com");

// Assert
Assert.AreEqual(store1, result);
_cacheMock.Verify(c => c.GetAsync(string.Format(CacheKey.STORES_BY_HOST_KEY, "store1.com"),
It.IsAny<Func<Task<Store>>>()), Times.Once);
}

[TestMethod]
Expand All @@ -108,15 +108,25 @@ public async Task GetStoreByHost_NoMatchingHost_ReturnsNullStore()
new DomainHost { HostName = "store2.com", Url = "http://store2.com", Primary = true }
};

var stores = new List<Store> { store1, store2 };

_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<Store>>>>()))
.Returns(Task.FromResult(stores));
_repository.Setup(c => c.Table).Returns(new List<Store> { store1, store2 }.AsQueryable());

// Act - host not found in any DomainHost
var result = await _service.GetStoreByHost("nonexisting.com");

// Assert - returns first store
Assert.IsNull(result);
}

[TestMethod]
public async Task GetFirstStore_ReturnsStoreWithLowestDisplayOrder()
{
var store1 = new Store { DisplayOrder = 2 };
var store2 = new Store { DisplayOrder = 1 };
_repository.Setup(c => c.Table).Returns(new List<Store> { store1, store2 }.AsQueryable());

var result = await _service.GetFirstStore();

Assert.AreEqual(store2, result);
_cacheMock.Verify(c => c.GetAsync(CacheKey.STORES_FIRST_KEY, It.IsAny<Func<Task<Store>>>()), Times.Once);
}
}
3 changes: 1 addition & 2 deletions src/Web/Grand.Web.Common/StoreContextSetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected async Task<Store> CurrentStore(string id = null)
}

// Fallback: return the first available store
return (await _storeService.GetAllStores()).FirstOrDefault();
return await _storeService.GetFirstStore();
}

/// <summary>
Expand Down Expand Up @@ -89,4 +89,3 @@ private sealed class CurrentStoreContext : IStoreContext
}
}


2 changes: 1 addition & 1 deletion src/Web/Grand.Web.Common/WorkContextSetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ protected async Task<Store> CurrentStore(string id = null)
if (!string.IsNullOrEmpty(id))
return await _storeService.GetStoreById(id);

return (await _storeService.GetAllStores()).FirstOrDefault();
return await _storeService.GetFirstStore();
}

private sealed class CurrentWorkContext : IWorkContext
Expand Down