Skip to content

Commit

Permalink
PT-1924: Add rounding policy (#189)
Browse files Browse the repository at this point in the history
* PT-1924: Add rounding policy
* PT-1924: Add unit tests
* PT-1924: Set Round Policy in CurrencyService
* PT-1924: Add RoundType and MidpointRounding properties to the blade
* PT-1924: Fix deserialization issue

Co-authored-by: Filipp Kuksov <[email protected]>
  • Loading branch information
krankenbro and Vectorfield4 authored Aug 5, 2021
1 parent 4ff78c9 commit 55e7c24
Show file tree
Hide file tree
Showing 16 changed files with 519 additions and 23 deletions.
28 changes: 24 additions & 4 deletions src/VirtoCommerce.CoreModule.Core/Currency/Currency.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
using VirtoCommerce.CoreModule.Core.Common;
using VirtoCommerce.Platform.Core.Common;
using MidpointRoundingEnum = System.MidpointRounding;
using RoundingTypeEnum = VirtoCommerce.CoreModule.Core.Enums.RoundingType;

namespace VirtoCommerce.CoreModule.Core.Currency
{
Expand Down Expand Up @@ -60,10 +62,8 @@ public Currency(Language language, string code)
public Currency()
: this(Language.InvariantLanguage, null)
{

}


/// <summary>
/// Currency code may be used ISO 4217.
/// </summary>
Expand All @@ -87,7 +87,6 @@ public string CultureName
}
}


public string EnglishName { get; set; }

[JsonIgnore]
Expand All @@ -97,14 +96,17 @@ public string CultureName
/// name of the currency
/// </summary>
public string Name { get; set; }

/// <summary>
/// Flag specifies that this is the primary currency
/// </summary>
public bool IsPrimary { get; set; }

/// <summary>
/// The exchange rate against the primary exchange rate of the currency.
/// </summary>
public decimal ExchangeRate { get; set; }

/// <summary>
/// Currency symbol
/// </summary>
Expand All @@ -115,6 +117,24 @@ public string CultureName
/// </summary>
public string CustomFormatting { get; set; }

[JsonIgnore]
public IMoneyRoundingPolicy RoundingPolicy { get; set; }

private RoundingTypeEnum _roundingType = RoundingTypeEnum.Rounding001;

public string RoundingType
{
get { return _roundingType.ToString(); }
set { _roundingType = EnumUtility.SafeParse(value, RoundingTypeEnum.Rounding001); }
}

private MidpointRoundingEnum _midpointRounding = MidpointRoundingEnum.AwayFromZero;

public string MidpointRounding
{
get { return _midpointRounding.ToString(); }
set { _midpointRounding = EnumUtility.SafeParse(value, MidpointRoundingEnum.AwayFromZero); }
}

protected override IEnumerable<object> GetEqualityComponents()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using VirtoCommerce.CoreModule.Core.Enums;
using VirtoCommerce.Platform.Core.Common;

namespace VirtoCommerce.CoreModule.Core.Currency
{
// TODO: Reduce the cognitive complexity
// https://github.com/grandnode/grandnode2/blob/master/src/Business/Grand.Business.Catalog/Extensions/RoundingHelper.cs#L44
public class DefaultMoneyRoundingPolicy : IMoneyRoundingPolicy
{
public decimal RoundMoney(decimal amount, Currency currency)
{
var roundingType = EnumUtility.SafeParse(currency.RoundingType, RoundingType.Rounding001);
var midpointRounding = EnumUtility.SafeParse(currency.MidpointRounding, MidpointRounding.AwayFromZero);
return Round(amount, currency.NumberFormat.NumberDecimalDigits, roundingType, midpointRounding);
}

/// <summary>
/// Round
/// </summary>
/// <param name="value">Value to round</param>
/// <param name="decimals">Number of digits in fractional part</param>
/// <param name="roundingType">The rounding type</param>
/// <param name="midpointRounding">Specifies how mathematical rounding methods should process a number that is midway between two numbers</param>
/// <returns>Rounded value</returns>
protected decimal Round(decimal value, int decimals, RoundingType roundingType, MidpointRounding midpointRounding)
{
//default round (Rounding001)
var result = Math.Round(value, decimals, midpointRounding);
var fractionPart = (result - Math.Truncate(result)) * 10;

//cash rounding not needed
if (fractionPart == 0)
return result;

//Cash rounding (details: https://en.wikipedia.org/wiki/Cash_rounding)
switch (roundingType)
{
//rounding with 0.05 or 5 intervals
case RoundingType.Rounding005Up:
case RoundingType.Rounding005Down:
fractionPart = (fractionPart - Math.Truncate(fractionPart)) * 10;
if (fractionPart == 5 || fractionPart == 0)
break;
if (roundingType == RoundingType.Rounding005Down)
fractionPart = fractionPart > 5 ? 5 - fractionPart : fractionPart * -1;
else
fractionPart = fractionPart > 5 ? 10 - fractionPart : 5 - fractionPart;

result += fractionPart / 100;
break;
//rounding with 0.10 intervals
case RoundingType.Rounding01Up:
case RoundingType.Rounding01Down:
fractionPart = (fractionPart - Math.Truncate(fractionPart)) * 10;
if (fractionPart == 0)
break;

if (roundingType == RoundingType.Rounding01Down && fractionPart == 5)
fractionPart = -5;
else
fractionPart = fractionPart < 5 ? fractionPart * -1 : 10 - fractionPart;

result += fractionPart / 100;
break;
//rounding with 0.50 intervals
case RoundingType.Rounding05:
fractionPart *= 10;
fractionPart = fractionPart < 25 ? fractionPart * -1 : fractionPart < 50 || fractionPart < 75 ? 50 - fractionPart : 100 - fractionPart;

result += fractionPart / 100;
break;
//rounding with 1.00 intervals
case RoundingType.Rounding1:
case RoundingType.Rounding1Up:
fractionPart *= 10;

if (roundingType == RoundingType.Rounding1Up && fractionPart > 0)
result = Math.Truncate(result) + 1;
else
result = fractionPart < 50 ? Math.Truncate(result) : Math.Truncate(result) + 1;

break;
case RoundingType.Rounding001:
default:
break;
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace VirtoCommerce.CoreModule.Core.Currency
{
public interface IMoneyRoundingPolicy
{
decimal RoundMoney(decimal amount, Currency currency);
}
}
2 changes: 1 addition & 1 deletion src/VirtoCommerce.CoreModule.Core/Currency/Money.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Money(decimal amount, Currency currency)
/// of the associated currency using MidpointRounding.AwayFromZero.
/// </summary>
/// <returns>A decimal with the amount rounded to the significant number of decimal digits.</returns>
public decimal Amount => decimal.Round(InternalAmount, DecimalDigits, MidpointRounding.AwayFromZero);
public decimal Amount => Currency.RoundingPolicy.RoundMoney(InternalAmount, Currency);

/// <summary>
/// Truncates the amount to the number of significant decimal digits
Expand Down
15 changes: 15 additions & 0 deletions src/VirtoCommerce.CoreModule.Core/Enums/RoundingType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace VirtoCommerce.CoreModule.Core.Enums
{
//Cash rounding (details: https://en.wikipedia.org/wiki/Cash_rounding)
public enum RoundingType
{
Rounding001,
Rounding005Up,
Rounding005Down,
Rounding01Up,
Rounding01Down,
Rounding05,
Rounding1,
Rounding1Up
}
}
12 changes: 11 additions & 1 deletion src/VirtoCommerce.CoreModule.Data/Currency/CurrencyEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public class CurrencyEntity : AuditableEntity

[StringLength(64)]
public string CustomFormatting { get; set; }
[StringLength(16)]
public string MidpointRounding { get; set; }
[StringLength(16)]
public string RoundingType { get; set; }

public virtual Core.Currency.Currency ToModel(Core.Currency.Currency currency)
{
Expand All @@ -33,6 +37,8 @@ public virtual Core.Currency.Currency ToModel(Core.Currency.Currency currency)
currency.ExchangeRate = ExchangeRate;
currency.Symbol = Symbol;
currency.CustomFormatting = CustomFormatting;
currency.MidpointRounding = MidpointRounding;
currency.RoundingType = RoundingType;
return currency;
}

Expand All @@ -44,17 +50,21 @@ public virtual CurrencyEntity FromModel(Core.Currency.Currency currency)
ExchangeRate = currency.ExchangeRate;
Symbol = currency.Symbol;
CustomFormatting = currency.CustomFormatting;
MidpointRounding = currency.MidpointRounding.ToString();
RoundingType = currency.RoundingType.ToString();
return this;
}

public virtual void Patch(CurrencyEntity target)
{
target.Code = Code;
target.Code = Code;
target.Name = Name;
target.IsPrimary = IsPrimary;
target.ExchangeRate = ExchangeRate;
target.Symbol = Symbol;
target.CustomFormatting = CustomFormatting;
target.MidpointRounding = MidpointRounding;
target.RoundingType = RoundingType;
}

}
Expand Down
11 changes: 9 additions & 2 deletions src/VirtoCommerce.CoreModule.Data/Currency/CurrencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ public class CurrencyService : ICurrencyService
private readonly Func<ICoreRepository> _repositoryFactory;
private readonly IEventPublisher _eventPublisher;
private readonly IPlatformMemoryCache _platformMemoryCache;
private readonly IMoneyRoundingPolicy _moneyRoundingPolicy;

public CurrencyService(Func<ICoreRepository> repositoryFactory, IEventPublisher eventPublisher, IPlatformMemoryCache platformMemoryCache)
public CurrencyService(Func<ICoreRepository> repositoryFactory, IEventPublisher eventPublisher, IPlatformMemoryCache platformMemoryCache, IMoneyRoundingPolicy moneyRoundingPolicy)
{
_repositoryFactory = repositoryFactory;
_eventPublisher = eventPublisher;
_platformMemoryCache = platformMemoryCache;
_moneyRoundingPolicy = moneyRoundingPolicy;
}

public async Task<IEnumerable<Core.Currency.Currency>> GetAllCurrenciesAsync()
Expand All @@ -34,7 +36,12 @@ public CurrencyService(Func<ICoreRepository> repositoryFactory, IEventPublisher
using (var repository = _repositoryFactory())
{
var currencyEntities = await repository.Currencies.OrderByDescending(x => x.IsPrimary).ThenBy(x => x.Code).ToArrayAsync();
var result = currencyEntities.Select(x => x.ToModel(AbstractTypeFactory<Core.Currency.Currency>.TryCreateInstance())).ToList();
var result = currencyEntities.Select(x =>
{
var currency = x.ToModel(AbstractTypeFactory<Core.Currency.Currency>.TryCreateInstance());
currency.RoundingPolicy = _moneyRoundingPolicy;
return currency;
}).ToList();

return result;
}
Expand Down
Loading

0 comments on commit 55e7c24

Please sign in to comment.