From aa911e94386bf5704412e4ebf829f9f42e1b42ee Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Date: Sat, 20 Jun 2026 15:50:33 -0400 Subject: [PATCH 1/4] Implementing product and rebate data stores --- .../Data/ProductDataStore.cs | 68 +++++++++++++++++-- .../Data/RebateDataStore.cs | 62 +++++++++++++++-- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/Smartwyre.DeveloperTest/Data/ProductDataStore.cs b/Smartwyre.DeveloperTest/Data/ProductDataStore.cs index 8b4fae4..c9d5a53 100644 --- a/Smartwyre.DeveloperTest/Data/ProductDataStore.cs +++ b/Smartwyre.DeveloperTest/Data/ProductDataStore.cs @@ -1,12 +1,72 @@ -using Smartwyre.DeveloperTest.Types; +using System.Collections.Generic; +using Smartwyre.DeveloperTest.Types; namespace Smartwyre.DeveloperTest.Data; -public class ProductDataStore +public interface IProductDataStore { + IEnumerable GetProducts(); + Product GetProduct(string productIdentifier); +} + +public class ProductDataStore : IProductDataStore +{ + private readonly Dictionary _products = new() + { + { + "product-fixed-rate", + new Product + { + Id = 1, + Identifier = "product-fixed-rate", + Price = 100m, + Uom = "Each", + SupportedIncentives = SupportedIncentiveType.FixedRateRebate + } + }, + { + "product-amount-per-uom", + new Product + { + Id = 2, + Identifier = "product-amount-per-uom", + Price = 25m, + Uom = "Case", + SupportedIncentives = SupportedIncentiveType.AmountPerUom + } + }, + { + "product-fixed-cash", + new Product + { + Id = 3, + Identifier = "product-fixed-cash", + Price = 50m, + Uom = "Each", + SupportedIncentives = SupportedIncentiveType.FixedCashAmount + } + }, + { + "product-all-incentives", + new Product + { + Id = 4, + Identifier = "product-all-incentives", + Price = 75m, + Uom = "Each", + SupportedIncentives = SupportedIncentiveType.FixedRateRebate + | SupportedIncentiveType.AmountPerUom + | SupportedIncentiveType.FixedCashAmount + } + } + }; + + public IEnumerable GetProducts() => _products.Values; + public Product GetProduct(string productIdentifier) { - // Access database to retrieve account, code removed for brevity - return new Product(); + _products.TryGetValue(productIdentifier, out var product); + + return product; } } diff --git a/Smartwyre.DeveloperTest/Data/RebateDataStore.cs b/Smartwyre.DeveloperTest/Data/RebateDataStore.cs index 3d88169..d374121 100644 --- a/Smartwyre.DeveloperTest/Data/RebateDataStore.cs +++ b/Smartwyre.DeveloperTest/Data/RebateDataStore.cs @@ -1,17 +1,69 @@ -using Smartwyre.DeveloperTest.Types; +using System; +using System.Collections.Generic; +using Smartwyre.DeveloperTest.Types; namespace Smartwyre.DeveloperTest.Data; -public class RebateDataStore +public interface IRebateDataStore { + IEnumerable GetRebates(); + Rebate GetRebate(string rebateIdentifier); + void StoreCalculationResult(Rebate account, decimal rebateAmount); +} + +public class RebateDataStore : IRebateDataStore +{ + private readonly Dictionary _rebates = new() + { + { + "rebate-fixed-rate", + new Rebate + { + Identifier = "rebate-fixed-rate", + Incentive = IncentiveType.FixedRateRebate, + Percentage = 0.1m + } + }, + { + "rebate-amount-per-uom", + new Rebate + { + Identifier = "rebate-amount-per-uom", + Incentive = IncentiveType.AmountPerUom, + Amount = 5m + } + }, + { + "rebate-fixed-cash", + new Rebate + { + Identifier = "rebate-fixed-cash", + Incentive = IncentiveType.FixedCashAmount, + Amount = 25m + } + } + }; + + private readonly List _rebateCalculations = new(); + + public IEnumerable GetRebates() => _rebates.Values; + public Rebate GetRebate(string rebateIdentifier) { - // Access database to retrieve account, code removed for brevity - return new Rebate(); + _rebates.TryGetValue(rebateIdentifier, out var rebate); + + return rebate; } public void StoreCalculationResult(Rebate account, decimal rebateAmount) { - // Update account in database, code removed for brevity + _rebateCalculations.Add(new RebateCalculation + { + Id = _rebateCalculations.Count + 1, + Identifier = Guid.NewGuid().ToString(), + RebateIdentifier = account.Identifier, + IncentiveType = account.Incentive, + Amount = rebateAmount + }); } } From 31f4637837e4b58d51f082748123beb3edfed473 Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Date: Sat, 20 Jun 2026 15:57:42 -0400 Subject: [PATCH 2/4] Refactor RebateSevice calculation using Strategy --- .../Calculators/AmountPerUomCalculator.cs | 22 ++++ .../Calculators/FixedCashAmountCalculator.cs | 21 ++++ .../Calculators/FixedRateRebateCalculator.cs | 23 ++++ .../Rebates/Calculators/IRebateCalculator.cs | 12 ++ .../Factories/IRebateCalculatorFactory.cs | 9 ++ .../Factories/RebateCalculatorFactory.cs | 27 ++++ .../Services/RebateService.cs | 116 ++++++------------ 7 files changed, 151 insertions(+), 79 deletions(-) create mode 100644 Smartwyre.DeveloperTest/Rebates/Calculators/AmountPerUomCalculator.cs create mode 100644 Smartwyre.DeveloperTest/Rebates/Calculators/FixedCashAmountCalculator.cs create mode 100644 Smartwyre.DeveloperTest/Rebates/Calculators/FixedRateRebateCalculator.cs create mode 100644 Smartwyre.DeveloperTest/Rebates/Calculators/IRebateCalculator.cs create mode 100644 Smartwyre.DeveloperTest/Rebates/Factories/IRebateCalculatorFactory.cs create mode 100644 Smartwyre.DeveloperTest/Rebates/Factories/RebateCalculatorFactory.cs diff --git a/Smartwyre.DeveloperTest/Rebates/Calculators/AmountPerUomCalculator.cs b/Smartwyre.DeveloperTest/Rebates/Calculators/AmountPerUomCalculator.cs new file mode 100644 index 0000000..52c32fd --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Calculators/AmountPerUomCalculator.cs @@ -0,0 +1,22 @@ +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Calculators; + +public class AmountPerUomCalculator : IRebateCalculator +{ + public IncentiveType IncentiveType => IncentiveType.AmountPerUom; + + public bool CanCalculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return rebate != null + && product != null + && product.SupportedIncentives.HasFlag(SupportedIncentiveType.AmountPerUom) + && rebate.Amount > 0 + && request.Volume > 0; + } + + public decimal Calculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return rebate.Amount * request.Volume; + } +} diff --git a/Smartwyre.DeveloperTest/Rebates/Calculators/FixedCashAmountCalculator.cs b/Smartwyre.DeveloperTest/Rebates/Calculators/FixedCashAmountCalculator.cs new file mode 100644 index 0000000..c2e233e --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Calculators/FixedCashAmountCalculator.cs @@ -0,0 +1,21 @@ +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Calculators; + +public class FixedCashAmountCalculator : IRebateCalculator +{ + public IncentiveType IncentiveType => IncentiveType.FixedCashAmount; + + public bool CanCalculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return rebate != null + && product != null + && product.SupportedIncentives.HasFlag(SupportedIncentiveType.FixedCashAmount) + && rebate.Amount > 0; + } + + public decimal Calculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return rebate.Amount; + } +} diff --git a/Smartwyre.DeveloperTest/Rebates/Calculators/FixedRateRebateCalculator.cs b/Smartwyre.DeveloperTest/Rebates/Calculators/FixedRateRebateCalculator.cs new file mode 100644 index 0000000..3e2217a --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Calculators/FixedRateRebateCalculator.cs @@ -0,0 +1,23 @@ +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Calculators; + +public class FixedRateRebateCalculator : IRebateCalculator +{ + public IncentiveType IncentiveType => IncentiveType.FixedRateRebate; + + public bool CanCalculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return rebate != null + && product != null + && product.SupportedIncentives.HasFlag(SupportedIncentiveType.FixedRateRebate) + && rebate.Percentage > 0 + && product.Price > 0 + && request.Volume > 0; + } + + public decimal Calculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + return product.Price * rebate.Percentage * request.Volume; + } +} diff --git a/Smartwyre.DeveloperTest/Rebates/Calculators/IRebateCalculator.cs b/Smartwyre.DeveloperTest/Rebates/Calculators/IRebateCalculator.cs new file mode 100644 index 0000000..f084631 --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Calculators/IRebateCalculator.cs @@ -0,0 +1,12 @@ +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Calculators; + +public interface IRebateCalculator +{ + IncentiveType IncentiveType { get; } + + bool CanCalculate(Rebate rebate, Product product, CalculateRebateRequest request); + + decimal Calculate(Rebate rebate, Product product, CalculateRebateRequest request); +} diff --git a/Smartwyre.DeveloperTest/Rebates/Factories/IRebateCalculatorFactory.cs b/Smartwyre.DeveloperTest/Rebates/Factories/IRebateCalculatorFactory.cs new file mode 100644 index 0000000..987d8e4 --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Factories/IRebateCalculatorFactory.cs @@ -0,0 +1,9 @@ +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Factories; + +public interface IRebateCalculatorFactory +{ + IRebateCalculator GetCalculator(IncentiveType incentiveType); +} diff --git a/Smartwyre.DeveloperTest/Rebates/Factories/RebateCalculatorFactory.cs b/Smartwyre.DeveloperTest/Rebates/Factories/RebateCalculatorFactory.cs new file mode 100644 index 0000000..945b7d6 --- /dev/null +++ b/Smartwyre.DeveloperTest/Rebates/Factories/RebateCalculatorFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Types; + +namespace Smartwyre.DeveloperTest.Rebates.Factories; + +public class RebateCalculatorFactory : IRebateCalculatorFactory +{ + private readonly IReadOnlyDictionary _calculators; + + public RebateCalculatorFactory(IEnumerable calculators) + { + _calculators = calculators.ToDictionary(calculator => calculator.IncentiveType); + } + + public IRebateCalculator GetCalculator(IncentiveType incentiveType) + { + if (!_calculators.TryGetValue(incentiveType, out var calculator)) + { + throw new NotSupportedException($"No rebate calculator registered for incentive type '{incentiveType}'."); + } + + return calculator; + } +} diff --git a/Smartwyre.DeveloperTest/Services/RebateService.cs b/Smartwyre.DeveloperTest/Services/RebateService.cs index 9248325..03885f1 100644 --- a/Smartwyre.DeveloperTest/Services/RebateService.cs +++ b/Smartwyre.DeveloperTest/Services/RebateService.cs @@ -1,99 +1,57 @@ -using Smartwyre.DeveloperTest.Data; +using System.Collections.Generic; +using Smartwyre.DeveloperTest.Data; +using Smartwyre.DeveloperTest.Rebates.Factories; using Smartwyre.DeveloperTest.Types; namespace Smartwyre.DeveloperTest.Services; public class RebateService : IRebateService { - public CalculateRebateResult Calculate(CalculateRebateRequest request) + private readonly IRebateDataStore _rebateDataStore; + private readonly IProductDataStore _productDataStore; + private readonly IRebateCalculatorFactory _rebateCalculatorFactory; + + public RebateService( + IRebateDataStore rebateDataStore, + IProductDataStore productDataStore, + IRebateCalculatorFactory rebateCalculatorFactory) { - var rebateDataStore = new RebateDataStore(); - var productDataStore = new ProductDataStore(); - - Rebate rebate = rebateDataStore.GetRebate(request.RebateIdentifier); - Product product = productDataStore.GetProduct(request.ProductIdentifier); + _rebateDataStore = rebateDataStore; + _productDataStore = productDataStore; + _rebateCalculatorFactory = rebateCalculatorFactory; + } - var result = new CalculateRebateResult(); + public CalculateRebateResult Calculate(CalculateRebateRequest request) + { + Rebate rebate = _rebateDataStore.GetRebate(request.RebateIdentifier); + Product product = _productDataStore.GetProduct(request.ProductIdentifier); - var rebateAmount = 0m; + //I added { Success = false } to improve readbility + var result = new CalculateRebateResult { Success = false }; - switch (rebate.Incentive) + if (rebate == null) { - case IncentiveType.FixedCashAmount: - if (rebate == null) - { - result.Success = false; - } - else if (!product.SupportedIncentives.HasFlag(SupportedIncentiveType.FixedCashAmount)) - { - result.Success = false; - } - else if (rebate.Amount == 0) - { - result.Success = false; - } - else - { - rebateAmount = rebate.Amount; - result.Success = true; - } - break; - - case IncentiveType.FixedRateRebate: - if (rebate == null) - { - result.Success = false; - } - else if (product == null) - { - result.Success = false; - } - else if (!product.SupportedIncentives.HasFlag(SupportedIncentiveType.FixedRateRebate)) - { - result.Success = false; - } - else if (rebate.Percentage == 0 || product.Price == 0 || request.Volume == 0) - { - result.Success = false; - } - else - { - rebateAmount += product.Price * rebate.Percentage * request.Volume; - result.Success = true; - } - break; + throw new KeyNotFoundException($"Rebate '{request.RebateIdentifier}' was not found."); + } - case IncentiveType.AmountPerUom: - if (rebate == null) - { - result.Success = false; - } - else if (product == null) - { - result.Success = false; - } - else if (!product.SupportedIncentives.HasFlag(SupportedIncentiveType.AmountPerUom)) - { - result.Success = false; - } - else if (rebate.Amount == 0 || request.Volume == 0) - { - result.Success = false; - } - else - { - rebateAmount += rebate.Amount * request.Volume; - result.Success = true; - } - break; + if (product == null) + { + throw new KeyNotFoundException($"Product '{request.ProductIdentifier}' was not found."); } - if (result.Success) + var calculator = _rebateCalculatorFactory.GetCalculator(rebate.Incentive); + + if (!calculator.CanCalculate(rebate, product, request)) { - var storeRebateDataStore = new RebateDataStore(); - storeRebateDataStore.StoreCalculationResult(rebate, rebateAmount); + return result; } + var rebateAmount = calculator.Calculate(rebate, product, request); + + _rebateDataStore.StoreCalculationResult(rebate, rebateAmount); + + result.Success = true; + return result; } } From 656ca4a894fb9f476e88bd4c6e65df4b3df673f6 Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Date: Sat, 20 Jun 2026 15:58:44 -0400 Subject: [PATCH 3/4] Add command-line execution to rebate runner --- Smartwyre.DeveloperTest.Runner/Program.cs | 119 +++++++++++++++++- .../Smartwyre.DeveloperTest.Runner.csproj | 8 ++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/Smartwyre.DeveloperTest.Runner/Program.cs b/Smartwyre.DeveloperTest.Runner/Program.cs index d3d77b7..763df0c 100644 --- a/Smartwyre.DeveloperTest.Runner/Program.cs +++ b/Smartwyre.DeveloperTest.Runner/Program.cs @@ -1,11 +1,126 @@ using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Smartwyre.DeveloperTest.Data; +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Rebates.Factories; +using Smartwyre.DeveloperTest.Services; +using Smartwyre.DeveloperTest.Types; namespace Smartwyre.DeveloperTest.Runner; class Program { - static void Main(string[] args) + static int Main(string[] args) { - throw new NotImplementedException(); + using var serviceProvider = ConfigureServices(); + + var rebateService = serviceProvider.GetRequiredService(); + var rebateDataStore = serviceProvider.GetRequiredService(); + var productDataStore = serviceProvider.GetRequiredService(); + + Console.WriteLine("This runner prompts for rebate input values and calculates the rebate."); + Console.WriteLine("Provide the rebate identifier, product identifier, and volume when prompted."); + + ShowAvailableProducts(productDataStore); + ShowAvailableRebates(rebateDataStore); + + var request = BuildRequest(); + if (request == null) + { + Console.WriteLine("Invalid input. Exiting."); + return 1; + } + + try + { + var result = rebateService.Calculate(request); + Console.WriteLine(); + Console.WriteLine("Rebate calculation result:"); + Console.WriteLine(result.Success ? " - Success" : " - No rebate was calculated for the provided request"); + Console.WriteLine(); + Console.WriteLine("Request details:"); + Console.WriteLine($" Rebate Identifier: {request.RebateIdentifier}"); + Console.WriteLine($" Product Identifier: {request.ProductIdentifier}"); + Console.WriteLine($" Volume: {request.Volume}"); + return result.Success ? 0 : 2; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static CalculateRebateRequest BuildRequest() + { + Console.WriteLine("Enter rebate details to calculate a rebate."); + Console.WriteLine("Press Enter without typing to cancel."); + + var rebateIdentifier = Prompt("Rebate Identifier"); + var productIdentifier = Prompt("Product Identifier"); + var volumeInput = Prompt("Volume"); + + if (string.IsNullOrWhiteSpace(rebateIdentifier) || string.IsNullOrWhiteSpace(productIdentifier) || string.IsNullOrWhiteSpace(volumeInput)) + { + Console.WriteLine("Rebate identifier, product identifier, and volume are required."); + return null; + } + + if (!decimal.TryParse(volumeInput, out var volume)) + { + Console.WriteLine($"Invalid volume '{volumeInput}'. Please provide a numeric value."); + return null; + } + + return new CalculateRebateRequest + { + RebateIdentifier = rebateIdentifier, + ProductIdentifier = productIdentifier, + Volume = volume + }; + } + + private static string Prompt(string label) + { + Console.Write(label + ": "); + return Console.ReadLine() ?? string.Empty; + } + + private static void ShowAvailableProducts(IProductDataStore productDataStore) + { + Console.WriteLine(); + Console.WriteLine("Available products:"); + foreach (var product in productDataStore.GetProducts()) + { + Console.WriteLine($" - Identifier: {product.Identifier}, Price: {product.Price:C}, UOM: {product.Uom}, Supported incentives: {product.SupportedIncentives}"); + } + Console.WriteLine(); + } + + private static void ShowAvailableRebates(IRebateDataStore rebateDataStore) + { + Console.WriteLine("Available rebates:"); + foreach (var rebate in rebateDataStore.GetRebates()) + { + Console.WriteLine($" - Identifier: {rebate.Identifier}, Incentive: {rebate.Incentive.ToString()}, Amount: { rebate.Amount:C}, Percentage: {rebate.Percentage}"); + } + Console.WriteLine(); + } + + private static ServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services.BuildServiceProvider(); } } diff --git a/Smartwyre.DeveloperTest.Runner/Smartwyre.DeveloperTest.Runner.csproj b/Smartwyre.DeveloperTest.Runner/Smartwyre.DeveloperTest.Runner.csproj index 53e8e9b..ab5eafe 100644 --- a/Smartwyre.DeveloperTest.Runner/Smartwyre.DeveloperTest.Runner.csproj +++ b/Smartwyre.DeveloperTest.Runner/Smartwyre.DeveloperTest.Runner.csproj @@ -5,4 +5,12 @@ net10.0 + + + + + + + + From 1ba73032045014a3a1b5e46f7d8ee6fc32730b98 Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Date: Sat, 20 Jun 2026 15:59:09 -0400 Subject: [PATCH 4/4] Add unit tests for rebate calculation strategies --- .../PaymentService.Tests.cs | 13 -- .../Calculators/RebateCalculatorTests.cs | 216 +++++++++++++++++ .../Factories/RebateCalculatorFactoryTests.cs | 36 +++ .../Services/RebateServiceTests.cs | 218 ++++++++++++++++++ .../Smartwyre.DeveloperTest.Tests.csproj | 4 + 5 files changed, 474 insertions(+), 13 deletions(-) delete mode 100644 Smartwyre.DeveloperTest.Tests/PaymentService.Tests.cs create mode 100644 Smartwyre.DeveloperTest.Tests/Rebates/Calculators/RebateCalculatorTests.cs create mode 100644 Smartwyre.DeveloperTest.Tests/Rebates/Factories/RebateCalculatorFactoryTests.cs create mode 100644 Smartwyre.DeveloperTest.Tests/Services/RebateServiceTests.cs diff --git a/Smartwyre.DeveloperTest.Tests/PaymentService.Tests.cs b/Smartwyre.DeveloperTest.Tests/PaymentService.Tests.cs deleted file mode 100644 index 4807970..0000000 --- a/Smartwyre.DeveloperTest.Tests/PaymentService.Tests.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Xunit; - -namespace Smartwyre.DeveloperTest.Tests; - -public class PaymentServiceTests -{ - [Fact] - public void Test1() - { - throw new NotImplementedException(); - } -} diff --git a/Smartwyre.DeveloperTest.Tests/Rebates/Calculators/RebateCalculatorTests.cs b/Smartwyre.DeveloperTest.Tests/Rebates/Calculators/RebateCalculatorTests.cs new file mode 100644 index 0000000..3e50e08 --- /dev/null +++ b/Smartwyre.DeveloperTest.Tests/Rebates/Calculators/RebateCalculatorTests.cs @@ -0,0 +1,216 @@ +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Types; +using Xunit; + +namespace Smartwyre.DeveloperTest.Tests.Rebates.Calculators; + +public class FixedCashAmountCalculatorTests +{ + private readonly FixedCashAmountCalculator _calculator = new(); + + [Fact] + public void CanCalculate_ReturnsTrue_WhenProductSupportsIncentiveAndAmountIsPositive() + { + var rebate = new Rebate { Amount = 25m }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.FixedCashAmount }; + + Assert.True(_calculator.CanCalculate(rebate, product, new CalculateRebateRequest())); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void CanCalculate_ReturnsFalse_WhenAmountIsNotPositive(decimal amount) + { + var rebate = new Rebate { Amount = amount }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.FixedCashAmount }; + + Assert.False(_calculator.CanCalculate(rebate, product, new CalculateRebateRequest())); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenRebateIsNull() + { + var product = new Product { SupportedIncentives = SupportedIncentiveType.FixedCashAmount }; + + Assert.False(_calculator.CanCalculate(null!, product, new CalculateRebateRequest())); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductIsNull() + { + var rebate = new Rebate { Amount = 25m }; + + Assert.False(_calculator.CanCalculate(rebate, null!, new CalculateRebateRequest())); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductDoesNotSupportIncentive() + { + var rebate = new Rebate { Amount = 25m }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.FixedRateRebate }; + + Assert.False(_calculator.CanCalculate(rebate, product, new CalculateRebateRequest())); + } + + [Fact] + public void Calculate_ReturnsRebateAmount() + { + var rebate = new Rebate { Amount = 25m }; + + var result = _calculator.Calculate(rebate, new Product(), new CalculateRebateRequest()); + + Assert.Equal(25m, result); + } +} + +public class FixedRateRebateCalculatorTests +{ + private readonly FixedRateRebateCalculator _calculator = new(); + + [Fact] + public void CanCalculate_ReturnsTrue_WhenAllInputsAreValid() + { + var rebate = new Rebate { Percentage = 0.1m }; + var product = new Product + { + Price = 100m, + SupportedIncentives = SupportedIncentiveType.FixedRateRebate + }; + var request = new CalculateRebateRequest { Volume = 5m }; + + Assert.True(_calculator.CanCalculate(rebate, product, request)); + } + + [Theory] + [InlineData(0, 100, 5)] + [InlineData(0.1, 0, 5)] + [InlineData(0.1, 100, 0)] + public void CanCalculate_ReturnsFalse_WhenPercentagePriceOrVolumeIsZero( + decimal percentage, decimal price, decimal volume) + { + var rebate = new Rebate { Percentage = percentage }; + var product = new Product + { + Price = price, + SupportedIncentives = SupportedIncentiveType.FixedRateRebate + }; + var request = new CalculateRebateRequest { Volume = volume }; + + Assert.False(_calculator.CanCalculate(rebate, product, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenRebateIsNull() + { + var product = new Product + { + Price = 100m, + SupportedIncentives = SupportedIncentiveType.FixedRateRebate + }; + var request = new CalculateRebateRequest { Volume = 5m }; + + Assert.False(_calculator.CanCalculate(null!, product, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductIsNull() + { + var rebate = new Rebate { Percentage = 0.1m }; + var request = new CalculateRebateRequest { Volume = 5m }; + + Assert.False(_calculator.CanCalculate(rebate, null!, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductDoesNotSupportIncentive() + { + var rebate = new Rebate { Percentage = 0.1m }; + var product = new Product + { + Price = 100m, + SupportedIncentives = SupportedIncentiveType.FixedCashAmount + }; + var request = new CalculateRebateRequest { Volume = 5m }; + + Assert.False(_calculator.CanCalculate(rebate, product, request)); + } + + [Fact] + public void Calculate_ReturnsPriceTimesPercentageTimesVolume() + { + var rebate = new Rebate { Percentage = 0.1m }; + var product = new Product { Price = 100m }; + var request = new CalculateRebateRequest { Volume = 5m }; + + var result = _calculator.Calculate(rebate, product, request); + + Assert.Equal(50m, result); + } +} + +public class AmountPerUomCalculatorTests +{ + private readonly AmountPerUomCalculator _calculator = new(); + + [Fact] + public void CanCalculate_ReturnsTrue_WhenAllInputsAreValid() + { + var rebate = new Rebate { Amount = 5m }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.AmountPerUom }; + var request = new CalculateRebateRequest { Volume = 10m }; + + Assert.True(_calculator.CanCalculate(rebate, product, request)); + } + + [Theory] + [InlineData(0, 10)] + [InlineData(5, 0)] + public void CanCalculate_ReturnsFalse_WhenAmountOrVolumeIsZero(decimal amount, decimal volume) + { + var rebate = new Rebate { Amount = amount }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.AmountPerUom }; + var request = new CalculateRebateRequest { Volume = volume }; + + Assert.False(_calculator.CanCalculate(rebate, product, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenRebateIsNull() + { + var product = new Product { SupportedIncentives = SupportedIncentiveType.AmountPerUom }; + var request = new CalculateRebateRequest { Volume = 10m }; + + Assert.False(_calculator.CanCalculate(null!, product, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductIsNull() + { + var rebate = new Rebate { Amount = 5m }; + var request = new CalculateRebateRequest { Volume = 10m }; + + Assert.False(_calculator.CanCalculate(rebate, null!, request)); + } + + [Fact] + public void CanCalculate_ReturnsFalse_WhenProductDoesNotSupportIncentive() + { + var rebate = new Rebate { Amount = 5m }; + var product = new Product { SupportedIncentives = SupportedIncentiveType.FixedCashAmount }; + var request = new CalculateRebateRequest { Volume = 10m }; + + Assert.False(_calculator.CanCalculate(rebate, product, request)); + } + + [Fact] + public void Calculate_ReturnsAmountTimesVolume() + { + var rebate = new Rebate { Amount = 5m }; + var request = new CalculateRebateRequest { Volume = 10m }; + + var result = _calculator.Calculate(rebate, new Product(), request); + + Assert.Equal(50m, result); + } +} diff --git a/Smartwyre.DeveloperTest.Tests/Rebates/Factories/RebateCalculatorFactoryTests.cs b/Smartwyre.DeveloperTest.Tests/Rebates/Factories/RebateCalculatorFactoryTests.cs new file mode 100644 index 0000000..131be9a --- /dev/null +++ b/Smartwyre.DeveloperTest.Tests/Rebates/Factories/RebateCalculatorFactoryTests.cs @@ -0,0 +1,36 @@ +using System; +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Rebates.Factories; +using Smartwyre.DeveloperTest.Types; +using Xunit; + +namespace Smartwyre.DeveloperTest.Tests.Rebates.Factories; + +public class RebateCalculatorFactoryTests +{ + private readonly RebateCalculatorFactory _factory = new(new IRebateCalculator[] + { + new FixedCashAmountCalculator(), + new FixedRateRebateCalculator(), + new AmountPerUomCalculator() + }); + + [Theory] + [InlineData(IncentiveType.FixedCashAmount, typeof(FixedCashAmountCalculator))] + [InlineData(IncentiveType.FixedRateRebate, typeof(FixedRateRebateCalculator))] + [InlineData(IncentiveType.AmountPerUom, typeof(AmountPerUomCalculator))] + public void GetCalculator_ReturnsRegisteredCalculator(IncentiveType incentiveType, Type expectedType) + { + var calculator = _factory.GetCalculator(incentiveType); + + Assert.IsType(expectedType, calculator); + } + + [Fact] + public void GetCalculator_ThrowsNotSupportedException_WhenIncentiveTypeIsNotRegistered() + { + var factory = new RebateCalculatorFactory(Array.Empty()); + + Assert.Throws(() => factory.GetCalculator(IncentiveType.FixedRateRebate)); + } +} diff --git a/Smartwyre.DeveloperTest.Tests/Services/RebateServiceTests.cs b/Smartwyre.DeveloperTest.Tests/Services/RebateServiceTests.cs new file mode 100644 index 0000000..3385d33 --- /dev/null +++ b/Smartwyre.DeveloperTest.Tests/Services/RebateServiceTests.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using Smartwyre.DeveloperTest.Data; +using Smartwyre.DeveloperTest.Rebates.Calculators; +using Smartwyre.DeveloperTest.Rebates.Factories; +using Smartwyre.DeveloperTest.Services; +using Smartwyre.DeveloperTest.Types; +using Xunit; + +namespace Smartwyre.DeveloperTest.Tests.Services; + +public class RebateServiceTests +{ + [Fact] + public void Calculate_ReturnsSuccessAndStoresResult_WhenCalculatorCanCalculate() + { + var rebateDataStore = new FakeRebateDataStore( + new Rebate + { + Identifier = "rebate-1", + Incentive = IncentiveType.FixedRateRebate + }); + var productDataStore = new FakeProductDataStore( + new Product { Identifier = "product-1" }); + var calculator = new FakeRebateCalculator + { + CanCalculateResult = true, + CalculateResult = 99m + }; + var service = CreateService(rebateDataStore, productDataStore, calculator); + + var result = service.Calculate(new CalculateRebateRequest + { + RebateIdentifier = "rebate-1", + ProductIdentifier = "product-1", + Volume = 5m + }); + + Assert.True(result.Success); + Assert.True(calculator.CanCalculateCalled); + Assert.True(calculator.CalculateCalled); + Assert.Equal(1, rebateDataStore.StoreCallCount); + Assert.Equal(99m, rebateDataStore.LastStoredAmount); + Assert.Equal("rebate-1", rebateDataStore.LastStoredRebate.Identifier); + } + + [Fact] + public void Calculate_ReturnsFailure_WhenCalculatorCannotCalculate() + { + var rebateDataStore = new FakeRebateDataStore( + new Rebate + { + Identifier = "rebate-1", + Incentive = IncentiveType.FixedRateRebate + }); + var productDataStore = new FakeProductDataStore( + new Product { Identifier = "product-1" }); + var calculator = new FakeRebateCalculator { CanCalculateResult = false }; + var service = CreateService(rebateDataStore, productDataStore, calculator); + + var result = service.Calculate(new CalculateRebateRequest + { + RebateIdentifier = "rebate-1", + ProductIdentifier = "product-1", + Volume = 5m + }); + + Assert.False(result.Success); + Assert.True(calculator.CanCalculateCalled); + Assert.False(calculator.CalculateCalled); + Assert.Equal(0, rebateDataStore.StoreCallCount); + } + + [Fact] + public void Calculate_ThrowsKeyNotFoundException_WhenRebateIsNotFound() + { + var calculator = new FakeRebateCalculator(); + var service = CreateService( + new FakeRebateDataStore(), + new FakeProductDataStore(new Product { Identifier = "product-1" }), + calculator); + + var exception = Assert.Throws(() => service.Calculate(new CalculateRebateRequest + { + RebateIdentifier = "missing-rebate", + ProductIdentifier = "product-1", + Volume = 5m + })); + + Assert.Contains("missing-rebate", exception.Message); + Assert.False(calculator.CanCalculateCalled); + Assert.False(calculator.CalculateCalled); + } + + [Fact] + public void Calculate_ThrowsKeyNotFoundException_WhenProductIsNotFound() + { + var calculator = new FakeRebateCalculator(); + var service = CreateService( + new FakeRebateDataStore(new Rebate + { + Identifier = "rebate-1", + Incentive = IncentiveType.FixedRateRebate + }), + new FakeProductDataStore(), + calculator); + + var exception = Assert.Throws(() => service.Calculate(new CalculateRebateRequest + { + RebateIdentifier = "rebate-1", + ProductIdentifier = "missing-product", + Volume = 5m + })); + + Assert.Contains("missing-product", exception.Message); + Assert.False(calculator.CanCalculateCalled); + Assert.False(calculator.CalculateCalled); + } + + private static RebateService CreateService( + FakeRebateDataStore rebateDataStore, + FakeProductDataStore productDataStore, + FakeRebateCalculator calculator) + { + return new RebateService( + rebateDataStore, + productDataStore, + new FakeRebateCalculatorFactory(calculator)); + } + + private sealed class FakeRebateCalculator : IRebateCalculator + { + public bool CanCalculateResult { get; set; } = true; + public decimal CalculateResult { get; set; } + + public bool CanCalculateCalled { get; private set; } + public bool CalculateCalled { get; private set; } + + public IncentiveType IncentiveType => IncentiveType.FixedRateRebate; + + public bool CanCalculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + CanCalculateCalled = true; + return CanCalculateResult; + } + + public decimal Calculate(Rebate rebate, Product product, CalculateRebateRequest request) + { + CalculateCalled = true; + return CalculateResult; + } + } + + private sealed class FakeRebateCalculatorFactory : IRebateCalculatorFactory + { + private readonly IRebateCalculator _calculator; + + public FakeRebateCalculatorFactory(IRebateCalculator calculator) + { + _calculator = calculator; + } + + public IRebateCalculator GetCalculator(IncentiveType incentiveType) => _calculator; + } + + private sealed class FakeRebateDataStore : IRebateDataStore + { + private readonly Dictionary _rebates = new(); + + public int StoreCallCount { get; private set; } + public decimal LastStoredAmount { get; private set; } + public Rebate LastStoredRebate { get; private set; } + + public FakeRebateDataStore(params Rebate[] rebates) + { + foreach (var rebate in rebates) + { + _rebates[rebate.Identifier] = rebate; + } + } + + public IEnumerable GetRebates() => _rebates.Values; + + public Rebate GetRebate(string rebateIdentifier) + { + _rebates.TryGetValue(rebateIdentifier, out var rebate); + return rebate; + } + + public void StoreCalculationResult(Rebate account, decimal rebateAmount) + { + StoreCallCount++; + LastStoredRebate = account; + LastStoredAmount = rebateAmount; + } + } + + private sealed class FakeProductDataStore : IProductDataStore + { + private readonly Dictionary _products = new(); + + public FakeProductDataStore(params Product[] products) + { + foreach (var product in products) + { + _products[product.Identifier] = product; + } + } + + public IEnumerable GetProducts() => _products.Values; + + public Product GetProduct(string productIdentifier) + { + _products.TryGetValue(productIdentifier, out var product); + return product; + } + } +} diff --git a/Smartwyre.DeveloperTest.Tests/Smartwyre.DeveloperTest.Tests.csproj b/Smartwyre.DeveloperTest.Tests/Smartwyre.DeveloperTest.Tests.csproj index da9e53f..5055f5e 100644 --- a/Smartwyre.DeveloperTest.Tests/Smartwyre.DeveloperTest.Tests.csproj +++ b/Smartwyre.DeveloperTest.Tests/Smartwyre.DeveloperTest.Tests.csproj @@ -18,4 +18,8 @@ + + + +