Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ where c.AppliedDiscounts.Any(x => x == discountId)
return await _productRepository.PagedAsync(query, pageIndex, pageSize);
}

/// <summary>
/// Counts products visible in the given store that are also visible in at least one other store
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <returns>Number of shared products</returns>
public virtual async Task<int> CountSharedProducts(string storeId)
{
var query = from p in _productRepository.Table
where !p.LimitedToStores || (p.Stores.Contains(storeId) && p.Stores.Count > 1)
select p;

return await _productRepository.CountAsync(query);
}


/// <summary>
/// Inserts a product
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ public class CurrencyService : ICurrencyService
/// <param name="currencyRepository">Currency repository</param>
/// <param name="aclService">ACL service</param>
/// <param name="currencySettings">Currency settings</param>
/// <param name="primaryCurrencySettings">Primary currency settings, store-scoped</param>
/// <param name="mediator">Mediator</param>
public CurrencyService(ICacheBase cacheBase,
IRepository<Currency> currencyRepository,
IAclService aclService,
CurrencySettings currencySettings,
PrimaryCurrencySettings primaryCurrencySettings,
IMediator mediator)
{
_cacheBase = cacheBase;
_currencyRepository = currencyRepository;
_aclService = aclService;
_currencySettings = currencySettings;
_primaryCurrencySettings = primaryCurrencySettings;
_mediator = mediator;
}

Expand All @@ -47,6 +50,7 @@ public CurrencyService(ICacheBase cacheBase,
private readonly ICacheBase _cacheBase;
private readonly IMediator _mediator;
private readonly CurrencySettings _currencySettings;
private readonly PrimaryCurrencySettings _primaryCurrencySettings;
private Currency _primaryCurrency;
private Currency _primaryExchangeRateCurrency;

Expand All @@ -66,12 +70,13 @@ public virtual Task<Currency> GetCurrencyById(string currencyId)
}

/// <summary>
/// Gets primary store currency
/// Gets primary store currency - the currency prices are stored in. PrimaryCurrencySettings is resolved for
/// the current store, so a store overriding it gets its own currency and every other store the global one.
/// </summary>
/// <returns>Currency</returns>
public async Task<Currency> GetPrimaryStoreCurrency()
{
return _primaryCurrency ??= await GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId);
return _primaryCurrency ??= await GetCurrencyById(_primaryCurrencySettings.CurrencyId);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public interface IProductService
/// <returns>Products</returns>
Task<IPagedList<Product>> GetProductsByDiscount(string discountId, int pageIndex = 0, int pageSize = int.MaxValue);

/// <summary>
/// Counts products visible in the given store that are also visible in at least one other store,
/// either because they are not limited to stores at all or because they are mapped to more stores.
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <returns>Number of shared products</returns>
Task<int> CountSharedProducts(string storeId);

/// <summary>
/// Inserts a product
/// </summary>
Expand Down
1 change: 0 additions & 1 deletion src/Core/Grand.Domain/Directory/CurrencySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace Grand.Domain.Directory;

public class CurrencySettings : ISettings
{
public string PrimaryStoreCurrencyId { get; set; }
public string PrimaryExchangeRateCurrencyId { get; set; }
public string ActiveExchangeRateProviderSystemName { get; set; }
public bool AutoUpdateEnabled { get; set; }
Expand Down
18 changes: 18 additions & 0 deletions src/Core/Grand.Domain/Directory/PrimaryCurrencySettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Grand.Domain.Configuration;

namespace Grand.Domain.Directory;

/// <summary>
/// The currency a store's prices are stored in.
/// Kept apart from <see cref="CurrencySettings" /> so a single store can override it: settings fall back to the
/// global value per object rather than per field, so a store-scoped override placed on that class would also
/// freeze its system-wide fields - the exchange rate currency, the rate provider and the auto update flag - for
/// the same store.
/// </summary>
public class PrimaryCurrencySettings : ISettings
{
/// <summary>
/// Gets or sets the identifier of the currency prices are stored in
/// </summary>
public string CurrencyId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,15 @@ await _settingRepository.SaveSetting(new LoyaltyPointsSettings {
});

await _settingRepository.SaveSetting(new CurrencySettings {
PrimaryStoreCurrencyId = _currencyRepository.Table.Single(c => c.CurrencyCode == "USD").Id,
PrimaryExchangeRateCurrencyId = _currencyRepository.Table.Single(c => c.CurrencyCode == "USD").Id,
ActiveExchangeRateProviderSystemName = "CurrencyExchange.MoneyConverter",
AutoUpdateEnabled = false
});

await _settingRepository.SaveSetting(new PrimaryCurrencySettings {
CurrencyId = _currencyRepository.Table.Single(c => c.CurrencyCode == "USD").Id
});

await _settingRepository.SaveSetting(new MeasureSettings {
BaseDimensionId = _measureDimensionRepository.Table.Single(m => m.SystemKeyword == "centimetres").Id,
BaseWeightId = _measureWeightRepository.Table.Single(m => m.SystemKeyword == "lb").Id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Grand.Data;
using Grand.Domain.Configuration;
using Grand.Domain.Directory;
using Grand.Infrastructure.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace Grand.Module.Migration.Migrations._2._4;

/// <summary>
/// Moves the primary store currency out of <see cref="CurrencySettings" /> into its own
/// <see cref="PrimaryCurrencySettings" />, so a single store can override it.
/// Settings fall back to the global value per object rather than per field, so leaving the primary currency on
/// <c>CurrencySettings</c> would mean a store-scoped override also froze that class's system-wide fields - the
/// exchange rate currency, the rate provider and the auto update flag - for the same store.
/// The old <c>PrimaryStoreCurrencyId</c> element is read straight out of the stored metadata, because the
/// property no longer exists on the class. It is left in the document: the global IgnoreExtraElementsConvention
/// skips it on read and it disappears the next time the settings are saved.
/// </summary>
public class MigrationPrimaryCurrencySetting : IMigration
{
public int Priority => 1;
public DbVersion Version => new(2, 4);
public Guid Identity => new("2F91B4C7-6E38-4A05-9D1B-C38E7A426D5F");
public string Name => "Move the primary store currency into PrimaryCurrencySettings 2.4";

/// <summary>
/// Upgrade process
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public bool UpgradeProcess(IServiceProvider serviceProvider)
{
var repository = serviceProvider.GetRequiredService<IRepository<Setting>>();
var logService = serviceProvider.GetRequiredService<ILogger<MigrationPrimaryCurrencySetting>>();

try
{
var sourceName = nameof(CurrencySettings).ToLowerInvariant();
var targetName = nameof(PrimaryCurrencySettings).ToLowerInvariant();

var currencySettings = repository.Table.Where(x => x.Name == sourceName).ToList();
var primaryCurrencySettings = repository.Table.Where(x => x.Name == targetName).ToList();

foreach (var setting in currencySettings)
{
var currencyId = ReadPrimaryStoreCurrencyId(setting.Metadata);
if (string.IsNullOrEmpty(currencyId))
{
logService.LogWarning(
"No primary store currency found in the currency settings of store {StoreId} - set it in the admin area, prices cannot be converted without it",
string.IsNullOrEmpty(setting.StoreId) ? "(all stores)" : setting.StoreId);
continue;
}

//never overwrite a value that is already there - the migration must be safe to re-run
if (primaryCurrencySettings.Any(x => x.StoreId == setting.StoreId))
continue;

repository.Insert(new Setting {
Name = targetName,
StoreId = setting.StoreId,
Metadata = JsonSerializer.Serialize(new PrimaryCurrencySettings { CurrencyId = currencyId })
});
}
}
catch (Exception ex)
{
logService.LogError(ex, "UpgradeProcess - MigrationPrimaryCurrencySetting (2.4)");
}
Comment on lines +68 to +71

return true;
}

private static string ReadPrimaryStoreCurrencyId(string metadata)
{
if (string.IsNullOrEmpty(metadata))
return null;

Check warning on line 79 in src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationPrimaryCurrencySetting.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 79 in src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationPrimaryCurrencySetting.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

using var document = JsonDocument.Parse(metadata);
return document.RootElement.TryGetProperty("PrimaryStoreCurrencyId", out var currencyId)

Check warning on line 82 in src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationPrimaryCurrencySetting.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 82 in src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationPrimaryCurrencySetting.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
? currencyId.GetString()
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public async Task<IActionResult> AddPopup()
{
var model = new ShippingByWeightModel {
PrimaryStoreCurrencyCode =
(await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode,
(await _currencyService.GetPrimaryStoreCurrency()).CurrencyCode,
BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name,
To = 1000000
};
Expand Down Expand Up @@ -255,7 +255,7 @@ public async Task<IActionResult> EditPopup(string id)
RatePerWeightUnit = sbw.RatePerWeightUnit,
LowerWeightLimit = sbw.LowerWeightLimit,
PrimaryStoreCurrencyCode =
(await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode,
(await _currencyService.GetPrimaryStoreCurrency()).CurrencyCode,
BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Grand.Business.Core.Interfaces.Catalog.Directory;
using Grand.Business.Core.Interfaces.Catalog.Directory;
using Grand.Business.Core.Interfaces.Checkout.Shipping;
using Grand.Business.Core.Interfaces.Common.Configuration;
using Grand.Business.Core.Interfaces.Common.Directory;
Expand Down Expand Up @@ -193,9 +193,8 @@ public async Task<IActionResult> AddPopup()
var model = new ShippingByWeightModel {
//the owner cannot create records for another store
StoreId = CurrentStoreId,
//CurrencySettings is resolved per current store, so this already honours a store override
PrimaryStoreCurrencyCode =
(await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode,
//PrimaryCurrencySettings is resolved per current store, so this already honours a store override
PrimaryStoreCurrencyCode = (await _currencyService.GetPrimaryStoreCurrency())?.CurrencyCode,
BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name,
To = 1000000
};
Expand Down Expand Up @@ -258,9 +257,8 @@ public async Task<IActionResult> EditPopup(string id)
PercentageRateOfSubtotal = sbw.PercentageRateOfSubtotal,
RatePerWeightUnit = sbw.RatePerWeightUnit,
LowerWeightLimit = sbw.LowerWeightLimit,
//CurrencySettings is resolved per current store, so this already honours a store override
PrimaryStoreCurrencyCode =
(await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode,
//PrimaryCurrencySettings is resolved per current store, so this already honours a store override
PrimaryStoreCurrencyCode = (await _currencyService.GetPrimaryStoreCurrency())?.CurrencyCode,
BaseWeightIn = (await _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId)).Name
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public void TestInitialize()
_currencyRepo,
_aclService,
_currencySettings,
new PrimaryCurrencySettings(),
null);

_taxSettings = new TaxSettings();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ public void TestInitialize()
_eventPublisher = eventPublisher.Object;

_currencySettings = new CurrencySettings {
PrimaryExchangeRateCurrencyId = "1",
PrimaryStoreCurrencyId = "1"
PrimaryExchangeRateCurrencyId = "1"
};

_currency = new Currency {
Expand Down Expand Up @@ -105,7 +104,7 @@ public void TestInitialize()

_currencyService = new CurrencyService(
cacheManager, _currencyRepository, _aclService,
_currencySettings, _eventPublisher);
_currencySettings, new PrimaryCurrencySettings { CurrencyId = "1" }, _eventPublisher);

tempDiscountApplicationService = new Mock<IDiscountHandlerService>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -862,4 +862,59 @@ public async Task InsertDiscountTest()
Assert.IsNotNull(result);
Assert.HasCount(1, result.AppliedDiscounts);
}

[TestMethod]
public async Task CountSharedProducts_ProductAvailableInEveryStore_IsCountedAsShared()
{
//Arrange
await _productRepository.InsertAsync(new Product { LimitedToStores = false });

//Act
var result = await _productService.CountSharedProducts("store-1");

//Assert
Assert.AreEqual(1, result);
}

[TestMethod]
public async Task CountSharedProducts_ProductLimitedToThisStoreOnly_IsNotCountedAsShared()
{
//Arrange
await _productRepository.InsertAsync(new Product
{ LimitedToStores = true, Stores = new List<string> { "store-1" } });

//Act
var result = await _productService.CountSharedProducts("store-1");

//Assert
Assert.AreEqual(0, result);
}

[TestMethod]
public async Task CountSharedProducts_ProductLimitedToThisStoreAndAnother_IsCountedAsShared()
{
//Arrange
await _productRepository.InsertAsync(new Product
{ LimitedToStores = true, Stores = new List<string> { "store-1", "store-2" } });

//Act
var result = await _productService.CountSharedProducts("store-1");

//Assert
Assert.AreEqual(1, result);
}

[TestMethod]
public async Task CountSharedProducts_ProductLimitedToAnotherStoreOnly_IsNotCountedAsShared()
{
//Arrange
await _productRepository.InsertAsync(new Product
{ LimitedToStores = true, Stores = new List<string> { "store-2", "store-3" } });

//Act
var result = await _productService.CountSharedProducts("store-1");

//Assert
Assert.AreEqual(0, result);
}
}
Loading
Loading