Created
April 11, 2016 00:34
-
-
Save kaldas/dae22a89654d0067c01abc266076c050 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define CONTRACTS_FULL | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics.Contracts; | |
using NUnit.Framework; | |
namespace EnergyQuoteService | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var quoteService = new QuoteService(new BasicRateCalculator()); | |
PopulateDemoTariff(quoteService); | |
var quoteRequest = GetDemoQuoteRequest(); | |
var cheapestQuote = quoteService.GetCheapestQuote(quoteRequest); | |
Console.WriteLine("Date: {0} Gas usage: {1} Electricity usage: {2} Cheapest tariff: {3} Annual cost: {4}", | |
cheapestQuote.QuoteCreationDate, cheapestQuote.GasUsage, cheapestQuote.ElectricityUsage, cheapestQuote.TariffName, | |
cheapestQuote.AnnualCost.ToString("F")); | |
Console.Read(); | |
} | |
public static QuoteRequest GetDemoQuoteRequest() | |
{ | |
var quoteRequest = new QuoteRequest | |
{ | |
AnnualElectricityUsage = 4000, | |
AnnualGasUsage = 2000, | |
Email = "[email protected]", | |
ValidFrom = DateTime.Today | |
}; | |
return quoteRequest; | |
} | |
public static void PopulateDemoTariff(QuoteService quoteService) | |
{ | |
var energySaver = new Tariff() | |
{ | |
Name = "Energy Saver", | |
GasInitialRate = 0.25M, | |
GasFinalRate = 0.50M, | |
ElectricityInitialRate = 0.30M, | |
ElectricityFinalRate = 0.60M, | |
InitialExpiryDate = new DateTime(2016, 06, 01) | |
}; | |
var discountEnergy = new Tariff() | |
{ | |
Name = "Discount Energy", | |
GasInitialRate = 0.20M, | |
GasFinalRate = 0.75M, | |
ElectricityInitialRate = 0.20M, | |
ElectricityFinalRate = 0.90M, | |
InitialExpiryDate = new DateTime(2016, 09, 15) | |
}; | |
var standard = new Tariff() | |
{ | |
Name = "Standard", | |
GasInitialRate = 0.65M, | |
ElectricityInitialRate = 0.80M, | |
}; | |
var saveOnline = new Tariff() | |
{ | |
Name = "Save Online", | |
GasInitialRate = 0.25M, | |
GasFinalRate = 0.60M, | |
ElectricityInitialRate = 0.10M, | |
ElectricityFinalRate = 1M, | |
InitialExpiryDate = new DateTime(2016, 08, 23) | |
}; | |
quoteService.AddTariff(energySaver); | |
quoteService.AddTariff(discountEnergy); | |
quoteService.AddTariff(standard); | |
quoteService.AddTariff(saveOnline); | |
} | |
} | |
/// <summary> | |
/// The QuoteService class is responsible for providing the Cheapest Quote | |
/// </summary> | |
public class QuoteService | |
{ | |
private readonly IRateCalculator _rateCalculator; | |
public List<Tariff> TariffList { get; } | |
private QuoteService() { /* this is a little neat trick to block incorrect instantiation :) */ } | |
/// <summary> | |
/// The default constructor for the QuoteService class | |
/// </summary> | |
/// <param name="rateCalculator"></param> | |
public QuoteService(IRateCalculator rateCalculator) | |
{ | |
_rateCalculator = rateCalculator; | |
TariffList = new List<Tariff>(); | |
} | |
/// <summary> | |
/// Adds a tariff to the list of known tariffs | |
/// </summary> | |
/// <param name="newTariff"></param> | |
public void AddTariff(Tariff newTariff) | |
{ | |
TariffList.Add(newTariff); | |
} | |
/// <summary> | |
/// Clears the list of registered tariffs | |
/// </summary> | |
public void ClearTariffList() | |
{ | |
TariffList.Clear(); | |
} | |
/// <summary> | |
/// Gets the final cost for a quote based on a tariff | |
/// </summary> | |
/// <param name="quoteRequest"></param> | |
/// <param name="tariff"></param> | |
/// <returns></returns> | |
public decimal GetFinalCost(QuoteRequest quoteRequest, Tariff tariff) | |
{ | |
var timeOnInitialRate = _rateCalculator.GetTimeOnInitialRate(quoteRequest.ValidFrom, tariff.InitialExpiryDate); | |
var initialElectricityRateUnitCost = _rateCalculator.GetRateUnitCost(timeOnInitialRate, quoteRequest.AnnualElectricityUsage, tariff.ElectricityInitialRate); | |
var initialGasRateUnitCost = _rateCalculator.GetRateUnitCost(timeOnInitialRate, quoteRequest.AnnualGasUsage, tariff.GasInitialRate); | |
var timeOnFinalUnitRate = _rateCalculator.GetTimeOnFinalRate(timeOnInitialRate); | |
var finalGasUnitRateCost = _rateCalculator.GetRateUnitCost(timeOnFinalUnitRate, quoteRequest.AnnualGasUsage, tariff.GasFinalRate); | |
var finalElectricityRateCost = _rateCalculator.GetRateUnitCost(timeOnFinalUnitRate,quoteRequest.AnnualElectricityUsage, tariff.ElectricityFinalRate); | |
var finalCost = initialElectricityRateUnitCost + initialGasRateUnitCost + finalElectricityRateCost + finalGasUnitRateCost; | |
return finalCost; | |
} | |
/// <summary> | |
/// Returns the cheapest quote for the desired quote request | |
/// </summary> | |
/// <param name="quoteRequest"></param> | |
/// <returns></returns> | |
public CheapestQuote GetCheapestQuote(QuoteRequest quoteRequest) | |
{ | |
#region Contract-Pre-Condition | |
if (TariffList.Count == 0) | |
{ | |
throw new ArgumentException("Cannot get cheapest tariff because there are no registered tariffs."); | |
} | |
#endregion | |
var cheapestQuote = new CheapestQuote(); | |
foreach (var tariff in TariffList) | |
{ | |
var finalCost = GetFinalCost(quoteRequest, tariff); | |
if(IsQuoteCheaper(cheapestQuote, finalCost)) | |
{ | |
cheapestQuote.AnnualCost = finalCost; | |
cheapestQuote.TariffName = tariff.Name; | |
cheapestQuote.ElectricityUsage = quoteRequest.AnnualElectricityUsage; | |
cheapestQuote.GasUsage = quoteRequest.AnnualGasUsage; | |
cheapestQuote.CustomerEmail = quoteRequest.Email; | |
} | |
} | |
return cheapestQuote; | |
} | |
/// <summary> | |
/// Returns true if a quote is cheaper | |
/// </summary> | |
/// <param name="cheapestQuote"></param> | |
/// <param name="finalCost"></param> | |
/// <returns></returns> | |
public bool IsQuoteCheaper(CheapestQuote cheapestQuote, decimal finalCost) | |
{ | |
if (decimal.Compare(cheapestQuote.AnnualCost, finalCost) > 0) | |
{ | |
return true; | |
} | |
if (cheapestQuote.TariffName == null) | |
{ | |
return true; | |
} | |
return false; | |
} | |
} | |
/// <summary> | |
/// The interface IRateCalculator defines the contract for a Rate Calculator | |
/// </summary> | |
public interface IRateCalculator | |
{ | |
decimal GetTimeOnInitialRate(DateTime startDate, DateTime endDate); | |
decimal GetTimeOnFinalRate(decimal daysInInitialRate); | |
decimal GetRateUnitCost(decimal days, decimal consumption, decimal rate); | |
} | |
/// <summary> | |
/// The BasicRateCalculator class is a concrete implementation containing the strategy | |
/// described in the technical exercise for calculating the quote. | |
/// </summary> | |
public sealed class BasicRateCalculator : IRateCalculator | |
{ | |
/// <summary> | |
/// Gets the ammount of running days between the start and end date | |
/// </summary> | |
/// <param name="startDate"></param> | |
/// <param name="endDate"></param> | |
public decimal GetTimeOnInitialRate(DateTime startDate, DateTime endDate) | |
{ | |
#region Contract-Post-Condition | |
Contract.Ensures(Contract.Result<decimal>() >= 0M); | |
#endregion | |
decimal timeOnInitialRate = 365; | |
if (endDate != DateTime.MinValue) | |
{ | |
var timeSpan = endDate - startDate; | |
timeOnInitialRate = Convert.ToDecimal(timeSpan.TotalDays); | |
} | |
return timeOnInitialRate; | |
} | |
/// <summary> | |
/// Gets the ammount of running days until the end of the year | |
/// </summary> | |
/// <param name="daysInInitialRate"></param> | |
/// <returns></returns> | |
public decimal GetTimeOnFinalRate(decimal daysInInitialRate) | |
{ | |
return 365M - daysInInitialRate; | |
} | |
/// <summary> | |
/// Gets the cost of the initial rate the client will pay | |
/// </summary> | |
/// <param name="days"></param> | |
/// <param name="consumption"></param> | |
/// <param name="rate"></param> | |
/// <returns></returns> | |
public decimal GetRateUnitCost(decimal days, decimal consumption, decimal rate) | |
{ | |
return days / 365M * consumption * rate; | |
} | |
} | |
/// <summary> | |
/// The QuoteRequest class represents a request of a quote to the system | |
/// </summary> | |
public class QuoteRequest | |
{ | |
#region Backing-Fields | |
private decimal _annualGasUsage; | |
private decimal _annualElectricityUsage; | |
private string _email; | |
#endregion | |
/// <summary> | |
/// The initial date this quote is valid from | |
/// </summary> | |
public DateTime ValidFrom { get; set; } | |
/// <summary> | |
/// The email address of the customer | |
/// </summary> | |
public string Email | |
{ | |
get { return _email; } | |
set | |
{ | |
#region Contract-Invariant | |
if (string.IsNullOrEmpty(value)) | |
{ | |
throw new ArgumentException("The email address cannot be null or empty.", nameof(Email)); | |
} | |
#endregion | |
_email = value; | |
} | |
} | |
/// <summary> | |
/// The annual gas usage of the customer | |
/// </summary> | |
public decimal AnnualGasUsage | |
{ | |
get { return _annualGasUsage; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value < decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(AnnualGasUsage), "The customer annual gas usage cannot be a negative value."); | |
} | |
#endregion | |
_annualGasUsage = value; | |
} | |
} | |
/// <summary> | |
/// The annual electricity usage of the customer | |
/// </summary> | |
public decimal AnnualElectricityUsage | |
{ | |
get { return _annualElectricityUsage; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value < decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(AnnualElectricityUsage), "The customer electricity usage cannot be a negative value."); | |
} | |
#endregion | |
_annualElectricityUsage = value; | |
} | |
} | |
} | |
/// <summary> | |
/// The Tariff class represents | |
/// </summary> | |
public class Tariff | |
{ | |
/// <summary> | |
/// The name of the Tariff (i.e EnergySaver, Standard) | |
/// </summary> | |
public string Name { get; set; } | |
#region Backing-Fields | |
private decimal _gasInitialRate; | |
private decimal _gasFinalRate; | |
private decimal _electricityInitialRate; | |
private decimal _electricityFinalRate; | |
#endregion | |
/// <summary> | |
/// The initial gas rate of the tariff | |
/// </summary> | |
public decimal GasInitialRate | |
{ | |
get { return _gasInitialRate; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value <= decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(GasInitialRate), "The initial gas rate must be positive and higher than zero."); | |
} | |
#endregion | |
_gasInitialRate = value; | |
} | |
} | |
/// <summary> | |
/// The final gas rate of the tariff | |
/// </summary> | |
public decimal GasFinalRate | |
{ | |
get { return _gasFinalRate; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value <= decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(GasFinalRate), "The final gas rate must be positive and higher than zero."); | |
} | |
#endregion | |
_gasFinalRate = value; | |
} | |
} | |
/// <summary> | |
/// The initial electricity rate of the tariff | |
/// </summary> | |
public decimal ElectricityInitialRate | |
{ | |
get { return _electricityInitialRate; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value <= decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(ElectricityInitialRate), "The electricity initial gas rate must be positive and higher than zero."); | |
} | |
#endregion | |
_electricityInitialRate = value; | |
} | |
} | |
/// <summary> | |
/// The final electricity rate of the tariff | |
/// </summary> | |
public decimal ElectricityFinalRate | |
{ | |
get { return _electricityFinalRate; } | |
set | |
{ | |
#region Contract-Invariant | |
if (value <= decimal.Zero) | |
{ | |
throw new ArgumentOutOfRangeException(nameof(ElectricityFinalRate), "The electricity final gas rate must be positive and higher than zero."); | |
} | |
#endregion | |
_electricityFinalRate = value; | |
} | |
} | |
/// <summary> | |
/// The date when this tariff will initially expire | |
/// </summary> | |
public DateTime InitialExpiryDate { get; set; } | |
} | |
/// <summary> | |
/// The CheapestQuote class is a simple Value Object for returning the cheapest tariff | |
/// </summary> | |
public class CheapestQuote | |
{ | |
public CheapestQuote() | |
{ | |
QuoteCreationDate = DateTime.Now; | |
} | |
/// <summary> | |
/// The name of the Cheapest Tariff | |
/// </summary> | |
public string TariffName { get; set; } | |
/// <summary> | |
/// The email address of the Customer | |
/// </summary> | |
public string CustomerEmail { get; set; } | |
/// <summary> | |
/// The annual cost of the tariff | |
/// </summary> | |
public decimal AnnualCost { get; set; } | |
/// <summary> | |
/// The electricity usage of the client | |
/// </summary> | |
public decimal ElectricityUsage { get; set; } | |
/// <summary> | |
/// The gas usage of the client | |
/// </summary> | |
public decimal GasUsage { get; set; } | |
/// <summary> | |
/// The date when the quote was generated | |
/// </summary> | |
public DateTime QuoteCreationDate { get; } | |
} | |
[TestFixture] | |
public class TestQuoteService | |
{ | |
private QuoteService _quoteService; | |
private QuoteRequest _testQuoteRequest; | |
private Tariff _testTariff; | |
[OneTimeSetUp] | |
public void SetUp() | |
{ | |
_quoteService = new QuoteService(new BasicRateCalculator()); | |
_testQuoteRequest = new QuoteRequest() | |
{ | |
Email = "[email protected]", | |
AnnualGasUsage = 1500M, | |
AnnualElectricityUsage = 3000M, | |
ValidFrom = new DateTime(2012, 12, 01) | |
}; | |
_testTariff = new Tariff() | |
{ | |
Name = "Example Saver", | |
GasInitialRate = 0.25M, | |
GasFinalRate = 0.50M, | |
ElectricityInitialRate = 0.30M, | |
ElectricityFinalRate = 0.60M, | |
InitialExpiryDate = new DateTime(2013, 06, 01) | |
}; | |
} | |
[Test] | |
public void TestGetFinalCost() | |
{ | |
var finalCost = _quoteService.GetFinalCost(_testQuoteRequest, _testTariff); | |
Assert.AreEqual("1914.25", finalCost.ToString("F")); | |
} | |
[Test] | |
public void TestExceptionEmptyTariffList() | |
{ | |
_quoteService.ClearTariffList(); | |
Assert.Throws<ArgumentException>(() => _quoteService.GetCheapestQuote(_testQuoteRequest)); | |
} | |
[Test] | |
public void TestGetCheapestRate() | |
{ | |
var quoteRequest = new QuoteRequest() | |
{ | |
Email = "[email protected]", | |
AnnualGasUsage = 1500M, | |
AnnualElectricityUsage = 3000M, | |
ValidFrom = new DateTime(2014, 01, 01) | |
}; | |
var energySaver = new Tariff() | |
{ | |
Name = "Energy Saver", | |
GasInitialRate = 0.25M, | |
GasFinalRate = 0.50M, | |
ElectricityInitialRate = 0.30M, | |
ElectricityFinalRate = 0.60M, | |
InitialExpiryDate = new DateTime(2014, 06, 01) | |
}; | |
var discountEnergy = new Tariff() | |
{ | |
Name = "Discount Energy", | |
GasInitialRate = 0.20M, | |
GasFinalRate = 0.75M, | |
ElectricityInitialRate = 0.20M, | |
ElectricityFinalRate = 0.90M, | |
InitialExpiryDate = new DateTime(2014, 09, 15) | |
}; | |
var standard = new Tariff() | |
{ | |
Name = "Standard", | |
GasInitialRate = 0.65M, | |
ElectricityInitialRate = 0.80M, | |
}; | |
var saveOnline = new Tariff() | |
{ | |
Name = "Save Online", | |
GasInitialRate = 0.25M, | |
GasFinalRate = 0.60M, | |
ElectricityInitialRate = 0.10M, | |
ElectricityFinalRate = 1M, | |
InitialExpiryDate = new DateTime(2014, 08, 23) | |
}; | |
_quoteService.AddTariff(energySaver); | |
_quoteService.AddTariff(discountEnergy); | |
_quoteService.AddTariff(standard); | |
_quoteService.AddTariff(saveOnline); | |
var cheapestQuote = _quoteService.GetCheapestQuote(quoteRequest); | |
Assert.AreEqual(cheapestQuote.TariffName, "Discount Energy"); | |
} | |
[Test] | |
public void TestIsQuoteCheaperTrue() | |
{ | |
var cheapestQuote = new CheapestQuote(); | |
cheapestQuote.TariffName = "Test"; | |
cheapestQuote.AnnualCost = 200M; | |
Assert.IsTrue(_quoteService.IsQuoteCheaper(cheapestQuote, 100M)); | |
} | |
[Test] | |
public void TestIsQuoteCheaperFalse() | |
{ | |
var cheapestQuote = new CheapestQuote(); | |
cheapestQuote.TariffName = "Test"; | |
cheapestQuote.AnnualCost = 200M; | |
Assert.IsFalse(_quoteService.IsQuoteCheaper(cheapestQuote, 300M)); | |
} | |
[Test] | |
public void TestIsQuoteCheaperThenBlankQuote() | |
{ | |
var cheapestQuote = new CheapestQuote(); | |
Assert.IsTrue(_quoteService.IsQuoteCheaper(cheapestQuote, 100M)); | |
} | |
} | |
[TestFixture] | |
public class TestQuoteRequest | |
{ | |
private QuoteRequest _quoteRequest; | |
[OneTimeSetUp] | |
public void SetUp() | |
{ | |
_quoteRequest = new QuoteRequest(); | |
} | |
[Test] | |
public void TestExceptionInvalidEmail() | |
{ | |
Assert.Throws<ArgumentException>(() => _quoteRequest.Email = ""); | |
} | |
[Test] | |
public void TestExceptionAnnualGasUsageOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _quoteRequest.AnnualGasUsage = -1M); | |
} | |
[Test] | |
public void TestExceptionAnnualElectricityUsageOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _quoteRequest.AnnualElectricityUsage = -1M); | |
} | |
[Test] | |
public void TestEmail() | |
{ | |
_quoteRequest.Email = "[email protected]"; | |
Assert.AreEqual("[email protected]", _quoteRequest.Email); | |
} | |
} | |
[TestFixture] | |
public class TestBasicRateCalculator | |
{ | |
private BasicRateCalculator _basicRateCalculator; | |
[OneTimeSetUp] | |
public void SetUp() | |
{ | |
_basicRateCalculator = new BasicRateCalculator(); | |
} | |
[Test] | |
public void TestGetTimeOnInitialRate() | |
{ | |
var startDate = new DateTime(2016, 01, 01); | |
var endDate = new DateTime(2016, 09, 05); | |
Assert.AreEqual(248M, _basicRateCalculator.GetTimeOnInitialRate(startDate, endDate)); | |
} | |
[Test] | |
public void TestGetTimeOnFinalRate() | |
{ | |
Assert.AreEqual(183M, _basicRateCalculator.GetTimeOnFinalRate(182)); | |
} | |
[Test] | |
public void TestGetRateUnitCost() | |
{ | |
var rate = _basicRateCalculator.GetRateUnitCost(182M, 1500M, 0.25M); | |
var rateFormatted = rate.ToString("F"); | |
Assert.AreEqual("186.99", rateFormatted); | |
} | |
} | |
[TestFixture] | |
public class TestTariff | |
{ | |
private Tariff _tariff; | |
[OneTimeSetUp] | |
public void SetUp() | |
{ | |
_tariff = new Tariff(); | |
} | |
[Test] | |
public void TestExceptionSetGasInitialRateOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _tariff.GasInitialRate = 0); | |
} | |
[Test] | |
public void TestExceptionSetGasFinalRateOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _tariff.GasFinalRate = 0); | |
} | |
[Test] | |
public void TestExceptionSetElectricityInitialRateOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _tariff.ElectricityInitialRate = 0); | |
} | |
[Test] | |
public void TestExceptionSetElectricityFinalRateOutOfRange() | |
{ | |
Assert.Throws<ArgumentOutOfRangeException>(() => _tariff.ElectricityFinalRate = 0); | |
} | |
[Test] | |
public void TestGetGasInitialRate() | |
{ | |
Assert.AreEqual(_tariff.GasInitialRate, default(decimal)); | |
} | |
[Test] | |
public void TestGetGasFinalRate() | |
{ | |
Assert.AreEqual(_tariff.GasFinalRate, default(decimal)); | |
} | |
[Test] | |
public void TestGetElectricityInitialRate() | |
{ | |
_tariff.ElectricityInitialRate = 1; | |
Assert.AreEqual(_tariff.ElectricityInitialRate, 1); | |
} | |
[Test] | |
public void TestGetElectricityFinalRate() | |
{ | |
Assert.AreEqual(_tariff.ElectricityFinalRate, default(decimal)); | |
} | |
[Test] | |
public void TestGetSetName() | |
{ | |
_tariff.Name = "TestTariff"; | |
Assert.AreEqual(_tariff.Name, "TestTariff"); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment