diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ce6fdde --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/CashRegister.sln b/CashRegister.sln new file mode 100644 index 00000000..1b36328e --- /dev/null +++ b/CashRegister.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29503.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CashRegister", "CashRegister\CashRegister.csproj", "{A04E856B-69A9-41E4-B164-A2ACDFB2D709}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CashRegisterTests", "CashRegisterTests\CashRegisterTests.csproj", "{369B12A2-ACEE-48AF-BAA8-690EF1EAD6C1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A04E856B-69A9-41E4-B164-A2ACDFB2D709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A04E856B-69A9-41E4-B164-A2ACDFB2D709}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A04E856B-69A9-41E4-B164-A2ACDFB2D709}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A04E856B-69A9-41E4-B164-A2ACDFB2D709}.Release|Any CPU.Build.0 = Release|Any CPU + {369B12A2-ACEE-48AF-BAA8-690EF1EAD6C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {369B12A2-ACEE-48AF-BAA8-690EF1EAD6C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {369B12A2-ACEE-48AF-BAA8-690EF1EAD6C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {369B12A2-ACEE-48AF-BAA8-690EF1EAD6C1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {84E678E1-0FBE-4993-B4DC-7A5354811E9D} + EndGlobalSection +EndGlobal diff --git a/CashRegister/CashRegister.csproj b/CashRegister/CashRegister.csproj new file mode 100644 index 00000000..3f0d6a5c --- /dev/null +++ b/CashRegister/CashRegister.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + + + diff --git a/CashRegister/CashRegister/Abstract/CashRegister.cs b/CashRegister/CashRegister/Abstract/CashRegister.cs new file mode 100644 index 00000000..e7c34038 --- /dev/null +++ b/CashRegister/CashRegister/Abstract/CashRegister.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Text; + +namespace CashRegisterConsumer +{ + public abstract class CashRegister + { + private ICurrency _currency; + private decimal _price, _tender; + private int _transactionCount; // for future use and current exception handling + private ITenderStrategy _tenderStrategy; + + public decimal PriceValue { get { return _price; } set { _price = value; } } + public decimal TenderValue { get { return _tender; } set { _tender = value; } } + + public CashRegister(ICurrency currency, ITenderStrategy tenderStrategy) + { + RegisterCurrency(currency); + RegisterTenderStrategy(tenderStrategy); + } + + public virtual void RegisterCurrency(ICurrency currency) + { + if (currency == null) + throw new NullReferenceException("Attempt to register a null currency is invalid."); + if (currency.AllDenominations.Count == 0) + throw new InvalidCurrencyException($"No denominations found in currency {currency.ToString()}"); + + this._currency = currency; + } + + public virtual void RegisterTenderStrategy(ITenderStrategy tenderStrategy) + { + if (tenderStrategy == null) + throw new NullReferenceException("Attempt to register a null tender strategy is invalid."); + + this._tenderStrategy = tenderStrategy; + } + + public string Tender(string path) + { + // ensure that the path is not null + if (path == null || path == string.Empty) + throw new FileNotFoundException("Input file not found.", path); + + // setup our stringbuilder for the response + StringBuilder tenderedValues = new StringBuilder(); + try + { + // using to ensure stream closure + using (StreamReader sr = new StreamReader(path, System.Text.Encoding.UTF8)) + { + // if the file is empty, throw format excpetion with message + if (sr.EndOfStream) + throw new FormatException($"The file {path} was empty"); + + while (!sr.EndOfStream) + { + // log transaction count (for exception handling) + _transactionCount++; + // setup the _price and _tender for this transaction + SetTransactionAmounts(sr); + // add the transaction calculation based on the strategy to the return string + var results = _tenderStrategy.Calculate(_currency, _price, _tender); + tenderedValues.Append(_tenderStrategy.Display(_currency)); + }; + } + + // return the tendered value string for all transactions + return tenderedValues.ToString(); + } + catch (FormatException e) // throw a generic message with a more specific inner message. + { + throw new FormatException($"The file {path} was not in the correct format", e); + } + catch (Exception e) // Is something missed? + { + throw e; + } + } + + private void SetTransactionAmounts(StreamReader sr) + { + try + { + // reset for new transaction + _currency.Clear(); + _price = _tender = 0; + + // read the next line and set the _price and _tender + // NOTE: I used the Parse over TryParse to ensure non-numeric values throw an exception + var input = sr.ReadLine(); + _price = Decimal.Parse(input.Split(",")[0]); + _tender = Decimal.Parse(input.Split(",")[1]); + + if (_tender <= 0 || _price <= 0) + throw new InvalidCurrencyException($"Invalid negative currency. Please review line {_transactionCount} for errors."); + + // ensure there is enough tender for the price. (they can be equal) + if (_tender < _price) + throw new NotEnoughTenderException($"Tender value less than price. Deficiency: {_price - _tender} on line {_transactionCount}"); + } + catch (OverflowException e) + { + throw new InvalidCurrencyException($"Invalid input. Please review line {_transactionCount} for errors.", e); + } + catch (NotEnoughTenderException e) + { + throw new NotEnoughTenderException($"Please review line {_transactionCount} for errors", e); + } + catch (Exception e) + { + throw new FormatException(String.Format("{0} : Line {1}", e.Message, _transactionCount)); + } + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Concrete/POSCashRegister.cs b/CashRegister/CashRegister/Concrete/POSCashRegister.cs new file mode 100644 index 00000000..15f1c669 --- /dev/null +++ b/CashRegister/CashRegister/Concrete/POSCashRegister.cs @@ -0,0 +1,9 @@ +namespace CashRegisterConsumer +{ + public class POSCashRegister : CashRegister + { + public POSCashRegister(ICurrency currency, ITenderStrategy tenderStrategy) : base(currency, tenderStrategy) + { + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Abstract/Currency.cs b/CashRegister/CashRegister/Currencies/Abstract/Currency.cs new file mode 100644 index 00000000..607769ad --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Abstract/Currency.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace CashRegisterConsumer +{ + public abstract class Currency : ICurrency + { + protected List _bills; + protected List _coins; + + public List Bills { get { return _bills; } } + public List Coins { get { return _coins; } } + public List AllDenominations { get { return _bills.Concat(_coins).ToList(); } } + + public Currency() + { + // initialize lists + this._bills = new List(); + this._coins = new List(); + + InitializeCurrency(); // load currency "money" based on inherited implementation + this._bills.Sort(); // ensure that we have the correct order of money + this._bills.Reverse(); // since the sort will make it small to large, we want large to small so foreach works easier. + this._coins.Sort(); + this._coins.Reverse(); + } + + protected abstract void InitializeCurrency(); + + public void Clear() + { + foreach (Money money in AllDenominations) + { + money.Clear(); + } + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Abstract/ICurrency.cs b/CashRegister/CashRegister/Currencies/Abstract/ICurrency.cs new file mode 100644 index 00000000..9ae31a45 --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Abstract/ICurrency.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace CashRegisterConsumer +{ + public interface ICurrency + { + List Bills { get; } + List Coins { get; } + List AllDenominations { get; } + + void Clear(); + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Abstract/IMoney.cs b/CashRegister/CashRegister/Currencies/Abstract/IMoney.cs new file mode 100644 index 00000000..72cbcdb2 --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Abstract/IMoney.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CashRegisterConsumer +{ + public interface IMoney + { + void Add(int count); + void Subtract(int count); + void Clear(); + } +} diff --git a/CashRegister/CashRegister/Currencies/Abstract/Money.cs b/CashRegister/CashRegister/Currencies/Abstract/Money.cs new file mode 100644 index 00000000..bcfe509d --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Abstract/Money.cs @@ -0,0 +1,62 @@ +using System; + +namespace CashRegisterConsumer +{ + public abstract class Money : IComparable + { + private int _count; + + private readonly decimal _denomination; + private readonly string _singleName; + private readonly string _pluralName; + + public decimal Denomination { get { return _denomination; } } + public string Name { get { return (_count == 1) ? _singleName : _pluralName; } } + public int Count { get { return _count; } } + + public Money(decimal denomination, string singleName, string pluralName) + { + this._denomination = denomination; + this._singleName = singleName; + this._pluralName = pluralName; + this._count = 0; + } + + public Money(decimal denomination, string singleName, string pluralName, int count) : this(denomination, singleName, pluralName) + { + this._count = count; + } + + public void Add(int count) + { + _count += count; + } + + public void Subtract(int count) + { + _count -= count; + } + + public void Clear() + { + _count = 0; + } + + /// + /// IComparable override. Used for sorting and reversing the currency to ensure the list of Money is + /// from largest denomination to smallest. + /// + /// + /// + public int CompareTo(object other) + { + if (other == null) return 1; + + Money otherMoney = other as Money; + if (otherMoney != null) + return this._denomination.CompareTo(otherMoney._denomination); + else + throw new ArgumentException("Object is not Money"); + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Concrete/Bill.cs b/CashRegister/CashRegister/Currencies/Concrete/Bill.cs new file mode 100644 index 00000000..ffc641f1 --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Concrete/Bill.cs @@ -0,0 +1,8 @@ +namespace CashRegisterConsumer +{ + public class Bill : Money + { + public Bill(decimal denomination, string singleName, string pluralName) : base(denomination, singleName, pluralName) { } + public Bill(decimal denomination, string singleName, string pluralName, int count) : base(denomination, singleName, pluralName, count) { } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Concrete/Coin.cs b/CashRegister/CashRegister/Currencies/Concrete/Coin.cs new file mode 100644 index 00000000..2bff0858 --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Concrete/Coin.cs @@ -0,0 +1,9 @@ +namespace CashRegisterConsumer +{ + public class Coin : Money + { + public Coin(decimal denomination, string singleName, string pluralName) : base(denomination, singleName, pluralName) { } + public Coin(decimal denomination, string singleName, string pluralName, int count) : base(denomination, singleName, pluralName, count) { } + + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Currencies/Concrete/USD.cs b/CashRegister/CashRegister/Currencies/Concrete/USD.cs new file mode 100644 index 00000000..d4d810ee --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Concrete/USD.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace CashRegisterConsumer +{ + internal class USD : Currency + { + public USD() : base() { } + + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(100000, "one hundred thousand", "one hundred thousands"), + new Bill(10000, "ten thousand", "ten thousands"), + new Bill(5000, "five thousand", "five thousands"), + new Bill(1000, "thousand", "thousands"), + new Bill(500, "five hundred", "five hundreds"), + new Bill(100, "hundred", "hundreds"), + new Bill(50, "fifty", "fifties"), + new Bill(20, "twenty", "twenties"), + new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars") + }; + + this._coins = new List() { + new Coin(.25m, "quarter", "quarters"), + new Coin(.10m, "dime", "dimes"), + new Coin(.05m, "nickel", "nickels"), + new Coin(.01m, "penny","pennies") + }; + } + } +} diff --git a/CashRegister/CashRegister/Currencies/Concrete/YEN.cs b/CashRegister/CashRegister/Currencies/Concrete/YEN.cs new file mode 100644 index 00000000..2738a91a --- /dev/null +++ b/CashRegister/CashRegister/Currencies/Concrete/YEN.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace CashRegisterConsumer +{ + class YEN : Currency + { + public YEN() : base() { } + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(10000, "10000-yen note", "10000-yen notes"), + new Bill(5000, "5000-yen note", "5000-yen notes"), + new Bill(2000, "2000-yen note", "2000-yen notes"), + new Bill(1000, "1000-yen note", "1000-yen notes") + }; + + this._coins = new List() { + new Coin(500, "500-yen coin", "500-yen coins"), + new Coin(100, "100-yen coin", "100-yen coins"), + new Coin(50, "50-yen coin", "50-yen coins"), + new Coin(10, "10-yen coin", "10-yen coins"), + new Coin(5, "5-yen coin", "5-yen coins"), + new Coin(1, "1-yen coin","1-yen coins") + }; + } + } +} diff --git a/CashRegister/CashRegister/Strategies/Abstract/ITenderStrategy.cs b/CashRegister/CashRegister/Strategies/Abstract/ITenderStrategy.cs new file mode 100644 index 00000000..9064aba1 --- /dev/null +++ b/CashRegister/CashRegister/Strategies/Abstract/ITenderStrategy.cs @@ -0,0 +1,9 @@ +namespace CashRegisterConsumer +{ + public interface ITenderStrategy + { + ICurrency Calculate(ICurrency currency, decimal price, decimal tender); + + string Display(ICurrency currency); + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Strategies/Abstract/TenderStrategy.cs b/CashRegister/CashRegister/Strategies/Abstract/TenderStrategy.cs new file mode 100644 index 00000000..939e11d7 --- /dev/null +++ b/CashRegister/CashRegister/Strategies/Abstract/TenderStrategy.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Text; + +namespace CashRegisterConsumer +{ + public abstract class TenderStrategy : ITenderStrategy + { + public virtual string Display(ICurrency currency) + { + // METHOD 1 - with foreach loop (did not remove code so reviewers could evaluate both methods) + //StringBuilder sr = new StringBuilder(); + //foreach (Money money in currency.AllDenominations) + //{ + // if (money.Count > 0) + // { + // sr.Append(String.Format("{0} {1},", money.Count, money.Name)); + // } + //} + + // METHOD 2 - Using LINQ.. both one line and 2 line versions. 1 line version commented out due to complexity and clarity + var money = currency.AllDenominations.Where(x => x.Count > 0); + var sr = money.Aggregate(new StringBuilder(), (x, y) => x.Append(String.Format("{0} {1},", y.Count, y.Name))); + //var sr = currency.AllDenominations.Where(x => x.Count > 0).Aggregate(new StringBuilder(), (x, y) => x.Append(String.Format("{0} {1},", y.Count, y.Name))); + + if (sr.Length == 0) + return String.Format("{0}\n", "No Change Due"); // not part of the requirements, yet exact change is a viable value. + else + return String.Format("{0}\n", sr.ToString().Trim(',')); + } + + public virtual ICurrency Calculate(ICurrency currency, decimal price, decimal tender) + { + if (currency.AllDenominations.Count == 0) + throw new InvalidCurrencyException("No currency denominations found"); + + decimal change = tender - price; + + // the currency.AllDenominations.Min(x => x.Denomination) is to ensure that if there is a currency that + // has a minimum value that is less then that change, the extra will be "dropped" and no infinite loop will occur. + // This itself has the problem that in a large system, these "dropped" percentages could be significant. This would + // be addressed with the business and the development team to determine the best course of action. + while (change >= currency.AllDenominations.Min(x => x.Denomination)) + { + foreach (Money money in currency.AllDenominations) + { + while (change >= money.Denomination) + { + money.Add(1); + change -= money.Denomination; + } + } + } + return currency; + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Strategies/Concrete/Random3Strategy.cs b/CashRegister/CashRegister/Strategies/Concrete/Random3Strategy.cs new file mode 100644 index 00000000..0161a7ea --- /dev/null +++ b/CashRegister/CashRegister/Strategies/Concrete/Random3Strategy.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; + +namespace CashRegisterConsumer +{ + public class Random3Strategy : TenderStrategy + { + public override ICurrency Calculate(ICurrency currency, decimal price, decimal tender) + { + if (currency.AllDenominations.Count == 0) + throw new InvalidCurrencyException("No currency denominations found"); + + if (Math.Abs(((price * 100) % 3)) == 0) // updated to correct algorithm + { + Random random = new Random(); + int count; + decimal change = tender - price; + + // the currency.AllDenominations.Min(x => x.Denomination) is to ensure that if there is a currency that + // has a minimum value that is less then that change, the extra will be "dropped" and no infinite loop will occur. + // This itself has the problem that in a large system, these "dropped" percentages could be significant. This would + // be addressed with the business and the development team to determine the best course of action. + while (change >= currency.AllDenominations.Min(x => x.Denomination)) + { + foreach (Money money in currency.AllDenominations) + { + if (money.Denomination <= change) // if the current denomination is less than the change + { + count = random.Next(1, (int)Math.Floor(change / money.Denomination)); // get a random count (not bigger than the change) + money.Add(count); // add the appropriate amount of this denomination based on our random + change -= (money.Denomination * count); // remove the money denomination(times count) from the change + } + } + } + return currency; + } + else + { + return base.Calculate(currency, price, tender); + } + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegister/Strategies/Concrete/StandardTenderStrategy.cs b/CashRegister/CashRegister/Strategies/Concrete/StandardTenderStrategy.cs new file mode 100644 index 00000000..2dc77065 --- /dev/null +++ b/CashRegister/CashRegister/Strategies/Concrete/StandardTenderStrategy.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace CashRegisterConsumer +{ + public class StandardTenderStrategy : TenderStrategy + { + public override ICurrency Calculate(ICurrency currency, decimal price, decimal tender) + { + return base.Calculate(currency, price, tender); + } + } +} \ No newline at end of file diff --git a/CashRegister/CashRegisterInputFiles/USD Input File.txt b/CashRegister/CashRegisterInputFiles/USD Input File.txt new file mode 100644 index 00000000..fe2de4c4 --- /dev/null +++ b/CashRegister/CashRegisterInputFiles/USD Input File.txt @@ -0,0 +1,10 @@ +2.12,3.00 +1.97,2.00 +3.33,5.00 +5.00,5.00 +8.83,1470.30 +20.57,76.55 +3.91,71.61 +48.31,78.81 +54.63,85.09 +62.33,95.61 \ No newline at end of file diff --git a/CashRegister/CashRegisterInputFiles/YEN Input File.txt b/CashRegister/CashRegisterInputFiles/YEN Input File.txt new file mode 100644 index 00000000..59bef7ac --- /dev/null +++ b/CashRegister/CashRegisterInputFiles/YEN Input File.txt @@ -0,0 +1,4 @@ +5000, 10000 +100, 1000 +350, 500 +120, 12000 \ No newline at end of file diff --git a/CashRegister/Custom Exceptions/InvalidCurrencyException.cs b/CashRegister/Custom Exceptions/InvalidCurrencyException.cs new file mode 100644 index 00000000..4e564e61 --- /dev/null +++ b/CashRegister/Custom Exceptions/InvalidCurrencyException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace CashRegisterConsumer +{ + /// + /// Custom exception for clarity of code and exception handling + /// + [Serializable] + public class InvalidCurrencyException : Exception + { + public InvalidCurrencyException() + { + } + + public InvalidCurrencyException(string message) : base(message) + { + } + + public InvalidCurrencyException(string message, Exception innerException) : base(message, innerException) + { + } + + protected InvalidCurrencyException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/CashRegister/Custom Exceptions/NotEnoughTenderException.cs b/CashRegister/Custom Exceptions/NotEnoughTenderException.cs new file mode 100644 index 00000000..4a04b778 --- /dev/null +++ b/CashRegister/Custom Exceptions/NotEnoughTenderException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace CashRegisterConsumer +{ + /// + /// Custom exception for clarity of code and exception handling + /// + [Serializable] + public class NotEnoughTenderException : Exception + { + public NotEnoughTenderException() + { + } + + public NotEnoughTenderException(string message) : base(message) + { + } + + public NotEnoughTenderException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NotEnoughTenderException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/CashRegister/Preface.txt b/CashRegister/Preface.txt new file mode 100644 index 00000000..5a9ce9ff --- /dev/null +++ b/CashRegister/Preface.txt @@ -0,0 +1,20 @@ +I have created this project with the idea that the console is the consumer of a CashRegister system that allows for the injection +of both the Currency type (USD, YEN, etc...) along with the strategy for change calculation. Future development would be able to allow +for different strategies to be injected using IOC containers in a configuration to quickly change the functionality and currency of the +CashRegister system. + +I have put more comments in this code than I normally do in production code as too many comments can be a code smell. I understand that +comments are important, but are best used sparingly. It is more important to use correct naming conventions and clear code that can be read +easily, then to trash code with a bunch of comments that with time may not be updated to reflect the actual working of the software. Code itself +should be the documentation. + +If there are any questions or comments please email me at plwest.axiom@gmail.com and I will respond at the earliest possible time. I can also provide +you with photos of my paperwork that includes my initial design notes, revision notes, and just my thoughts as I was working through this project. + +I appreciate your evaluation and giving me this oppritunity. Thank you. + + + +Press any button to continue with using the CashRegister Module. (Simulates POS ""Tender"" action). + + diff --git a/CashRegister/Program.cs b/CashRegister/Program.cs new file mode 100644 index 00000000..d992646e --- /dev/null +++ b/CashRegister/Program.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; + +namespace CashRegisterConsumer +{ + internal class Program + { + private static readonly string prefaceFilePath = @"C:\Users\plwes\source\repos\CashRegister\CashRegister\Preface.txt"; + private static readonly string usdInputFilePath = @"C:\Users\plwes\source\repos\CashRegister\CashRegister\CashRegisterInputFiles\USD Input File.txt"; + private static readonly string yenInputFilePath = @"C:\Users\plwes\source\repos\CashRegister\CashRegister\CashRegisterInputFiles\YEN Input File.txt"; + private static void Main(string[] args) + { + using (StreamReader sr = new StreamReader(prefaceFilePath, System.Text.Encoding.UTF8)) + { + Console.SetWindowSize(150, 25); + Console.WriteLine(sr.ReadToEnd()); + } + + Console.ReadKey(); + + try + { + CashRegister register = new POSCashRegister(new USD(), new Random3Strategy()); + var result = register.Tender(usdInputFilePath); + Console.WriteLine(result); + + + // for example (change the register to use YEN with a Standard Tender Strategy note that the demoniations are different so + // the same files that use "USD" decimals will be off. There are no decimals in YEN + register.RegisterCurrency(new YEN()); + register.RegisterTenderStrategy(new StandardTenderStrategy()); + result = register.Tender(yenInputFilePath); + Console.WriteLine(result); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + if (e.InnerException != null) + Console.WriteLine(e.InnerException.Message); + } + + + Console.ReadKey(); + } + } +} \ No newline at end of file diff --git a/CashRegister/_diagramCashRegister.cd b/CashRegister/_diagramCashRegister.cd new file mode 100644 index 00000000..eb6c679a --- /dev/null +++ b/CashRegister/_diagramCashRegister.cd @@ -0,0 +1,126 @@ + + + + + + + + + AgAAAAAAAAAAEAAAAIAAIABBAAAIAAAAgIAAAEAAAAg= + CashRegister\Abstract\CashRegister.cs + + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CashRegister\Concrete\POSCashRegister.cs + + + + + + AAAABAAAAAAAAAAAAAAADAAACAAAAAAAAAAACABEAAA= + CashRegister\Currencies\Abstract\Currency.cs + + + + + + + + + + + + + + ABIAAAAAAAAAAEQAAAACAAQAAAAAAAAABACAAABBAAA= + CashRegister\Currencies\Abstract\Money.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CashRegister\Currencies\Concrete\Bill.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CashRegister\Currencies\Concrete\Coin.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAA= + CashRegister\Currencies\Concrete\USD.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAA= + CashRegister\Currencies\Concrete\YEN.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAQAAACAAAAAAAAAAAAAAAAA= + CashRegister\Strategies\Abstract\TenderStrategy.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAA= + CashRegister\Strategies\Concrete\Random3Strategy.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAA= + CashRegister\Strategies\Concrete\StandardTenderStrategy.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Custom Exceptions\InvalidCurrencyException.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Custom Exceptions\NotEnoughTenderException.cs + + + + + + AAAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAABAAAA= + CashRegister\Currencies\Abstract\ICurrency.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAQAAACAAAAAAAAAAAAAAAAA= + CashRegister\Strategies\Abstract\ITenderStrategy.cs + + + + \ No newline at end of file diff --git a/CashRegisterTests/CashRegisterTests.cs b/CashRegisterTests/CashRegisterTests.cs new file mode 100644 index 00000000..e5cbdd0f --- /dev/null +++ b/CashRegisterTests/CashRegisterTests.cs @@ -0,0 +1,131 @@ +using CashRegisterConsumer; +using Moq; +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace CashRegisterTests +{ + public class CashRegisterTests + { + #region SETUP + + private readonly Mock currencyMock; + private readonly Mock tenderStrategyMock; + + private readonly string TenderValueTestFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\ValueTests\TenderValueTestFile.txt"; + private readonly string PriceValueTestFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\ValueTests\PriceValueTestFile.txt"; + private readonly string EmptyFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\EmptyFile.txt"; + private readonly string EmptyLineFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\EmptyLineFile.txt"; + private readonly string NotEnoughTenderFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\TenderLessThanPrice.txt"; + private readonly string OverflowFile = @"C:\Users\plwes\source\repos\CashRegister\CashRegisterTests\Test Input Files\ValueTests\OverflowTransactionTestFile.txt"; + public CashRegisterTests() + { + currencyMock = new Mock(); + tenderStrategyMock = new Mock(); + } + + #endregion SETUP + + [Fact] + public void CashRegisterPriceValueIsAccuratelySetBasedOnTextFileInput() + { + // setup + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Coin(.25m, "testCoin", "testCoins") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + + // setup excpeted + decimal expected = 2.45m; + // execute to a point where the actual can be tested + var results = register.Tender(PriceValueTestFile); + // are they equal???? + Assert.Equal(expected, register.PriceValue); + } + + [Fact] + public void CashRegisterTenderValueIsAccuratelySetBasedOnTextFileInput() + { + // setup + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Coin(.25m, "testCoin", "testCoins") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + + // setup excpeted + decimal expected = 110.98m; + // execute to a point where the actual can be tested + var results = register.Tender(TenderValueTestFile); + // are they equal???? + Assert.Equal(expected, register.TenderValue); + } + + #region Exception Tests + [Fact] + public void CashRegisterThrowOverflowExceptionGivenInputLargerThanLargestDecimalValue() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender(OverflowFile)); + } + + [Fact] + public void CashRegisterThrowsFileNotFoundExceptionGivenEmptyOrNullPath() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender("")); + } + + [Fact] + public void CashRegisterThrowsFileNotFoundExceptionGivenNullPath() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender(null)); + } + + [Fact] + public void CashRegisterThrowsFormatExceptionWhenEmptyFileIsProvided() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender(EmptyFile)); + } + + [Fact] + public void CashRegisterThrowsFormatExceptionWhenEmptyLineFoundInFileProvided() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender(EmptyLineFile)); + } + + [Fact] + public void CashRegisterThrowsNotEnoughTenderExceptionWhenTenderIsLessThanPrice() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1m, "testMoney", "testMonies") }); + CashRegister register = new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object); + Assert.Throws(() => register.Tender(NotEnoughTenderFile)); + } + + [Fact] + public void CashRegisterThrowsInvalidCurrencyExceptionWhenNoDenominationsFound() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List()); + Assert.Throws(() => new POSCashRegister(currencyMock.Object, tenderStrategyMock.Object)); + } + + [Fact] + public void CashRegisterThrowsNullReferenceExceptionWhenAttemptingToRegisterNullCurrency() + { + Assert.Throws(() => new POSCashRegister(null, tenderStrategyMock.Object)); + } + + [Fact] + public void CashRegisterThrowsNullReferenceExceptionWhenAttemptingToRegisterNullTenderStrategy() + { + Assert.Throws(() => new POSCashRegister(currencyMock.Object, null)); + } + + #endregion Exception Tests + } +} \ No newline at end of file diff --git a/CashRegisterTests/CashRegisterTests.csproj b/CashRegisterTests/CashRegisterTests.csproj new file mode 100644 index 00000000..134918e5 --- /dev/null +++ b/CashRegisterTests/CashRegisterTests.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.0 + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/CashRegisterTests/CurrencyTests.cs b/CashRegisterTests/CurrencyTests.cs new file mode 100644 index 00000000..b34da92b --- /dev/null +++ b/CashRegisterTests/CurrencyTests.cs @@ -0,0 +1,179 @@ +using CashRegisterConsumer; +using System.Collections.Generic; +using Xunit; + +namespace CurrencyTests +{ + public class CurrencyTests + { + #region Setup + + public CurrencyTests() + { + } + + #endregion Setup + + [Fact] + public void CurrencyClearClearsMoneyCountForBills() + { + Currency currency = new CurrencyTestSortReverseCurrency(); + + //now that we know we have a count in each of our bills... we will clear the currency and test. + currency.Clear(); + + foreach (Money bill in currency.Bills) + { + Assert.True(bill.Count == 0); + } + } + + [Fact] + public void CurrencyClearClearsMoneyCountForCoins() + { + Currency currency = new CurrencyTestSortReverseCurrency(); + + //now that we know we have a count in each of our coins... we will clear the currency and test. + currency.Clear(); + + foreach (Money coin in currency.Bills) + { + Assert.True(coin.Count == 0); + } + } + + [Fact] + public void CurrencyAllDenominationsReturnsConcatForBillsAndCoins() + { + // this also effectively tests the "sort/reverse" functionality of the InitializeCurrency method + // so creating a new method for that would be redundent (not necessarily bad though) + Currency currency = new CurrencyTestPluralNameCurrencyNoMoney(); + + for (int i = 0; i < currency.Bills.Count - 1; i++) + { + Assert.Equal(currency.Bills[i], currency.AllDenominations[i]); + } + + for (int i = 0; i < currency.Coins.Count - 1; i++) + { + Assert.Equal(currency.Coins[i], currency.AllDenominations[i + currency.Bills.Count]); // coins should start after bills due to the sort/reverse (denomination based) + } + } + } + + #region CurrecyTestClass + + public class CurrencyTestSortReverseCurrency : Currency + { + public CurrencyTestSortReverseCurrency() : base() + { + } + + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(5, "five", "fives"), + new Bill(100000, "one hundred thousand", "one hundred thousands"), + new Bill(50, "fifty", "fifties"), + new Bill(1000, "thousand", "thousands"), + new Bill(500, "five hundred", "five hundreds"), + new Bill(5000, "five thousand", "five thousands"), + new Bill(100, "hundred", "hundreds"), + new Bill(20, "twenty", "twenties"), + new Bill(10000, "ten thousand", "ten thousands"), + new Bill(10, "ten", "tens"), + new Bill(1, "dollar", "dollars") + }; + + this._coins = new List() { + new Coin(.05m, "nickel", "nickels"), + new Coin(.01m, "penny","pennies"), + new Coin(.25m, "quarter", "quarters"), + new Coin(.10m, "dime", "dimes") + }; + } + } + + public class CurrencyTestSingularNameCurrency : Currency + { + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(100000, "one hundred thousand", "one hundred thousands", 1), + new Bill(10000, "ten thousand", "ten thousands", 1), + new Bill(5000, "five thousand", "five thousands", 1), + new Bill(1000, "thousand", "thousands", 1), + new Bill(500, "five hundred", "five hundreds", 1), + new Bill(100, "hundred", "hundreds", 1), + new Bill(50, "fifty", "fifties", 1), + new Bill(20, "twenty", "twenties", 1), + new Bill(10, "ten", "tens", 1), + new Bill(5, "five", "fives", 1), + new Bill(1, "dollar", "dollars", 1) + }; + + this._coins = new List() { + new Coin(.25m, "quarter", "quarters", 1), + new Coin(.10m, "dime", "dimes", 1), + new Coin(.05m, "nickel", "nickels", 1), + new Coin(.01m, "penny","pennies", 1) + }; + } + } + + public class CurrencyTestPluralNameCurrency : Currency + { + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(100000, "one hundred thousand", "one hundred thousands", 50), + new Bill(10000, "ten thousand", "ten thousands", 2), + new Bill(5000, "five thousand", "five thousands", 2), + new Bill(1000, "thousand", "thousands", 4), + new Bill(500, "five hundred", "five hundreds", 2), + new Bill(100, "hundred", "hundreds", 600), + new Bill(50, "fifty", "fifties", 56), + new Bill(20, "twenty", "twenties", 2), + new Bill(10, "ten", "tens", 4), + new Bill(5, "five", "fives", 3), + new Bill(1, "dollar", "dollars", 1245) + }; + + this._coins = new List() { + new Coin(.25m, "quarter", "quarters", 500), + new Coin(.10m, "dime", "dimes", 5), + new Coin(.05m, "nickel", "nickels", 40), + new Coin(.01m, "penny","pennies", 100) + }; + } + } + + public class CurrencyTestPluralNameCurrencyNoMoney : Currency + { + protected override void InitializeCurrency() + { + this._bills = new List() { + new Bill(100000, "one hundred thousand", "one hundred thousands"), + new Bill(10000, "ten thousand", "ten thousands"), + new Bill(5000, "five thousand", "five thousands"), + new Bill(1000, "thousand", "thousands"), + new Bill(500, "five hundred", "five hundreds"), + new Bill(100, "hundred", "hundreds"), + new Bill(50, "fifty", "fifties"), + new Bill(20, "twenty", "twenties"), + new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars") + }; + + this._coins = new List() { + new Coin(.25m, "quarter", "quarters"), + new Coin(.10m, "dime", "dimes"), + new Coin(.05m, "nickel", "nickels"), + new Coin(.01m, "penny","pennies") + }; + } + } + + #endregion CurrecyTestClass +} \ No newline at end of file diff --git a/CashRegisterTests/MoneyTests.cs b/CashRegisterTests/MoneyTests.cs new file mode 100644 index 00000000..786835e1 --- /dev/null +++ b/CashRegisterTests/MoneyTests.cs @@ -0,0 +1,131 @@ +using CashRegisterConsumer; +using Moq; +using Xunit; + +namespace MoneyTests +{ + public class MoneyTests + { + #region Setup + + public MoneyTests() + { + } + + #endregion Setup + + [Fact] + public void MoneyConstructionSetsDenominationAccurately() + { + decimal expected = 0.01m; + Money money = new MoneyTestMoney(0.01m, "test", "tests", 3); + + Assert.Equal(expected, money.Denomination); + } + + [Fact] + public void MoneyConstructionSetsSingularNameAccurately() + { + string expected = "test"; + Money money = new MoneyTestMoney(0.01m, "test", "tests", 1); + + Assert.Equal(expected, money.Name); + } + + [Fact] + public void MoneyConstructionSetsPluralNameAccurately() + { + string expected = "tests"; + Money money = new MoneyTestMoney(0.01m, "test", "tests"); + + Assert.Equal(expected, money.Name); + } + + [Fact] + public void MoneyConstructionSetsPluralNameWithZeroCount() + { + string expected = "tests"; + Money money = new MoneyTestMoney(0.01m, "test", "tests"); + + Assert.Equal(expected, money.Name); + } + + [Fact] + public void MoneyAddsCorrectAmount() + { + Money money = new MoneyTestMoney(1, "test", "tests"); + Assert.Equal(0, money.Count); + + money.Add(1); + Assert.Equal(1, money.Count); + + money.Add(-1); + Assert.Equal(0, money.Count); + } + + [Fact] + public void MoneySubtractsCorrectAmount() + { + Money money = new MoneyTestMoney(1, "test", "tests", 5); + Assert.Equal(5, money.Count); + + money.Subtract(1); + Assert.Equal(4, money.Count); + + money.Subtract(-1); + Assert.Equal(5, money.Count); + } + + [Fact] + public void MoneyClearsCount() + { + Money money = new MoneyTestMoney(1, "test", "tests", 5); + Assert.Equal(5, money.Count); + + money.Clear(); + Assert.Equal(0, money.Count); + } + + [Fact] + public void MoneyCompareToReturnsZeroWhenEqual() + { + Mock moneyToCompare = new Mock(1m, "compare", "compares"); + Mock moneyToCompareTo = new Mock(1m, "compare", "compares"); + + Assert.Equal(0, moneyToCompare.Object.CompareTo(moneyToCompareTo.Object)); + } + + [Fact] + public void MoneyCompareToReturnsOneWhenMoreThan() + { + Mock moneyToCompare = new Mock(1m, "compare", "compares"); + Mock moneyToCompareTo = new Mock(0.50m, "compare", "compares"); + + Assert.Equal(1, moneyToCompare.Object.CompareTo(moneyToCompareTo.Object)); + } + + [Fact] + public void MoneyCompareToReturnsMinusOneWhenLessThan() + { + Mock moneyToCompare = new Mock(0.5m, "compare", "compares"); + Mock moneyToCompareTo = new Mock(1m, "compare", "compares"); + + Assert.Equal(-1, moneyToCompare.Object.CompareTo(moneyToCompareTo.Object)); + } + } + + #region MoneyTestClass + + public class MoneyTestMoney : Money + { + public MoneyTestMoney(decimal denomination, string singleName, string pluralName) : base(denomination, singleName, pluralName) + { + } + + public MoneyTestMoney(decimal denomination, string singleName, string pluralName, int count) : base(denomination, singleName, pluralName, count) + { + } + } + + #endregion MoneyTestClass +} \ No newline at end of file diff --git a/CashRegisterTests/Random3StrategyTests.cs b/CashRegisterTests/Random3StrategyTests.cs new file mode 100644 index 00000000..ab62bbce --- /dev/null +++ b/CashRegisterTests/Random3StrategyTests.cs @@ -0,0 +1,244 @@ +using CashRegisterConsumer; +using Moq; +using System; +using System.Collections.Generic; +using Xunit; + +namespace TenderStrategyTests +{ + public class Random3StrategyTests + { + #region Setup + + private readonly Mock mockCurrency; + + public Random3StrategyTests() + { + mockCurrency = new Mock(); + } + + #endregion Setup + + [Fact] + public void Random3TenderStrategyCalculatesChangeCorrectlyBasedOnPriceAndTenderDifference() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() + { new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars"), + new Coin(.10m,"dime","dimes"), + new Coin(.05m,"nickel","nickels"), + new Coin(.01m,"penny","pennies") + }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + decimal expected = 3001.50m; + decimal actual = 0.0m; + var result = tenderStrategy.Calculate(mockCurrency.Object, 123.03m, 3124.53m); // 3001.50 in change + foreach (Money money in mockCurrency.Object.AllDenominations) + { + actual += (money.Count * money.Denomination); + } + Assert.Equal(expected, actual); + + ClearCurrency(result); + expected = 0.0m; + actual = 0.0m; + + result = tenderStrategy.Calculate(mockCurrency.Object, 1.03m, 1.03m); // 0 in change + foreach (Money money in mockCurrency.Object.AllDenominations) + { + actual += (money.Count * money.Denomination); + } + Assert.Equal(expected, actual); + } + + [Fact] + public void Random3CalculatesExactTenderCorrectly() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() + { new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars"), + new Coin(.10m,"dime","dimes"), + new Coin(.05m,"nickel","nickels"), + new Coin(.01m,"penny","pennies") + }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + decimal expected = 0.0m; + decimal actual = 0.0m; + var result = tenderStrategy.Calculate(mockCurrency.Object, 1.03m, 1.03m); // 0 in change + foreach (Money money in mockCurrency.Object.AllDenominations) + { + actual += (money.Count * money.Denomination); + } + Assert.Equal(expected, actual); + } + + [Fact] + public void Random3CalculatesZeroTenderWhenTenderIsLessThanPrice() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() + { new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars"), + new Coin(.10m,"dime","dimes"), + new Coin(.05m,"nickel","nickels"), + new Coin(.01m,"penny","pennies") + }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + decimal expected = 0.0m; + decimal actual = 0.0m; + var result = tenderStrategy.Calculate(mockCurrency.Object, 10.03m, 1.03m); // 0 in change + foreach (Money money in result.AllDenominations) // mockCurrency.Object.AllDenominations) + { + actual += (money.Count * money.Denomination); + } + Assert.Equal(expected, actual); + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnNonDecimalDenominationsAndRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1.33m, 3.33m); + + // test that our "1 dollar bill" is added 2 times during the process for standard strategy for change + // (meaning that the price is recuded each time accordingly) + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(2, money.Count); // price - tender = 2 dollars in change + } + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnDecimalDenominationsAndRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.01m, "penny", "pennies") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1.33m, 1.43m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(10, money.Count); // price - tender = 10 pennies in change + } + + + ClearCurrency(actual); + actual = tenderStrategy.Calculate(mockCurrency.Object, 3.00m, 5.00m); + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(200, money.Count); // price - tender = 200 pennies in change + } + + ClearCurrency(actual); + actual = tenderStrategy.Calculate(mockCurrency.Object, 6.00m, 7.00m); + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(100, money.Count); // price - tender = 100 pennies in change + } + } + + private void ClearCurrency(ICurrency currency) + { + foreach (Money money in currency.AllDenominations) + { + money.Clear(); + } + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnOddDecimalDenominationsAndRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.25m, "quarter", "quarters") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1.33m, 2.33m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(4, money.Count); // price - tender = 4 quarters in change + } + + ClearCurrency(actual); + actual = tenderStrategy.Calculate(mockCurrency.Object, 3.00m, 5.00m); + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(8, money.Count); // price - tender = 8 quarters in change + } + + ClearCurrency(actual); + actual = tenderStrategy.Calculate(mockCurrency.Object, 6.00m, 7.00m); + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(4, money.Count); // price - tender = 4 quarters in change + } + } + + [Fact] + public void Random3TenderStrategyReturnsCurrentyWithNoMoneyValuesAndNonRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "test", "tests") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 0, 0); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(0, money.Count); // due to the "ToString" override being used, we have to test the counts. + } // Consider refactoring ToString. Hard testing indicates a design flaw. + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnNonDecimalDenominationsAndNonRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 3); + + // test that our "1 dollar bill" is added 2 times during the process for standard strategy for change + // (meaning that the price is recuded each time accordingly) + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(2, money.Count); // price - tender = 2 dollars in change + } + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnDecimalDenominationsAndNonRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.01m, "penny", "pennies") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 1.10m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(10, money.Count); // price - tender = 10 pennies in change + } + } + + [Fact] + public void Random3TenderStrategySubtractsCorrectlyFromThePriceBasedOnOddDecimalDenominationsAndNonRandomReturn() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.25m, "quarter", "quarters") }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 2.00m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(4, money.Count); // price - tender = 4 quarters in change + } + } + + + #region Exception Testing + + [Fact] + public void Random3TenderStrategyCalculateThrowsInvalidCurrencyExceptionWhenNoDenominationsFound() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { }); + ITenderStrategy tenderStrategy = new Random3Strategy(); + Assert.Throws(() => tenderStrategy.Calculate(mockCurrency.Object, 0, 0)); + } + + #endregion Exception Testing + } +} \ No newline at end of file diff --git a/CashRegisterTests/StandardTenderStrategyTests.cs b/CashRegisterTests/StandardTenderStrategyTests.cs new file mode 100644 index 00000000..e35dd0e3 --- /dev/null +++ b/CashRegisterTests/StandardTenderStrategyTests.cs @@ -0,0 +1,109 @@ +using CashRegisterConsumer; +using Moq; +using System.Collections.Generic; +using Xunit; + +namespace TenderStrategyTests +{ + public class StandardTenderStrategyTests + { + #region Setup + + private readonly Mock mockCurrency; + + public StandardTenderStrategyTests() + { + mockCurrency = new Mock(); + } + + #endregion Setup + + [Fact] + public void StandardCalculatesZeroTenderWhenTenderIsLessThanPrice() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() + { new Bill(10, "ten", "tens"), + new Bill(5, "five", "fives"), + new Bill(1, "dollar", "dollars"), + new Coin(.10m,"dime","dimes"), + new Coin(.05m,"nickel","nickels"), + new Coin(.01m,"penny","pennies") + }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + decimal expected = 0.0m; + decimal actual = 0.0m; + var result = tenderStrategy.Calculate(mockCurrency.Object, 10.03m, 1.03m); // 0 in change + foreach (Money money in mockCurrency.Object.AllDenominations) + { + actual += (money.Count * money.Denomination); + } + Assert.Equal(expected, actual); + } + + [Fact] + public void StandardTenderStrategyReturnsCurrentyWithNoMoneyValues() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "test", "tests") }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 0, 0); + + foreach (Money money in actual.AllDenominations)// mockCurrency.Object.AllDenominations) + { + Assert.Equal(0, money.Count); // due to the "ToString" override being used, we have to test the counts. + } // Consider refactoring ToString. Hard testing indicates a design flaw. + } + + [Fact] + public void StandardTenderStrategySubtractsCorrectlyFromThePriceBasedOnNonDecimalDenominations() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars") }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 3); + + // test that our "1 dollar bill" is added 2 times during the process for standard strategy for change + // (meaning that the price is recuded each time accordingly) + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(2, money.Count); // price - tender = 2 dollars in change + } + } + + [Fact] + public void StandardTenderStrategySubtractsCorrectlyFromThePriceBasedOnDecimalDenominations() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.01m, "penny", "pennies") }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 1.10m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(10, money.Count); // price - tender = 10 pennies in change + } + } + + [Fact] + public void StandardTenderStrategySubtractsCorrectlyFromThePriceBasedOnOddDecimalDenominations() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { new Bill(.25m, "quarter", "quarters") }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + var actual = tenderStrategy.Calculate(mockCurrency.Object, 1, 2.00m); + + foreach (Money money in actual.AllDenominations) // mockCurrency.Object.AllDenominations) + { + Assert.Equal(4, money.Count); // price - tender = 4 quarters in change + } + } + + #region Exception Testing + + [Fact] + public void StandardTenderStrategyCalculateThrowsInvalidCurrencyExceptionWhenNoDenominationsFound() + { + mockCurrency.Setup(p => p.AllDenominations).Returns(new List() { }); + ITenderStrategy tenderStrategy = new StandardTenderStrategy(); + Assert.Throws(() => tenderStrategy.Calculate(mockCurrency.Object, 0, 0)); + } + + #endregion Exception Testing + } +} \ No newline at end of file diff --git a/CashRegisterTests/TenderStrategyTests.cs b/CashRegisterTests/TenderStrategyTests.cs new file mode 100644 index 00000000..62914ce8 --- /dev/null +++ b/CashRegisterTests/TenderStrategyTests.cs @@ -0,0 +1,87 @@ +using CashRegisterConsumer; +using Moq; +using System; +using System.Collections.Generic; +using Xunit; + +namespace TenderStrategyTests +{ + public class TenderStrategyTests + { + #region Setup + + private readonly Mock currencyMock; + + public TenderStrategyTests() + { + currencyMock = new Mock(); + } + + #endregion Setup + + [Fact] + public void TenderStrategyDisplayReturnsProperlyFormattedStringValueForSingularCount() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars", 1) }); + string expected = "1 dollar\n"; + + TenderStrategy tenderStrategy = new TenderStrategyTestMock(); + var actual = tenderStrategy.Display(currencyMock.Object); + + Assert.Equal(expected, actual); + } + + [Fact] + public void TenderStrategyDisplayReturnsProperlyFormattedStringValueForMultipleCount() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars", 2) }); + string expected = "2 dollars\n"; + + TenderStrategy tenderStrategy = new TenderStrategyTestMock(); + var actual = tenderStrategy.Display(currencyMock.Object); + + Assert.Equal(expected, actual); + } + + [Fact] + public void TenderStrategyDisplayReturnsProperlyFormattedStringValueForZeroCount() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { new Bill(1, "dollar", "dollars", 0) }); + string expected = "No Change Due\n"; + + TenderStrategy tenderStrategy = new TenderStrategyTestMock(); + var actual = tenderStrategy.Display(currencyMock.Object); + + Assert.Equal(expected, actual); + } + + [Fact] + public void TenderStrategyDisplayReturnsProperStringValueForMultipleDenominations() + { + currencyMock.Setup(p => p.AllDenominations).Returns(new List() { + new Bill(5, "five dollar", "five dollars", 10), + new Bill(1, "dollar", "dollars", 1), + new Coin(.01m, "dime", "dimes",3), + new Coin(.01m,"penny","pennies",1) + }); + string expected = "10 five dollars,1 dollar,3 dimes,1 penny\n"; + + TenderStrategy tenderStrategy = new TenderStrategyTestMock(); + var actual = tenderStrategy.Display(currencyMock.Object); + + Assert.Equal(expected, actual); + } + } + + #region MockTenderStrategy Abstract + + internal class TenderStrategyTestMock : TenderStrategy + { + public override ICurrency Calculate(ICurrency currency, decimal price, decimal tender) + { + throw new NotImplementedException(); + } + } + + #endregion MockTenderStrategy Abstract +} \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/EmptyFile.txt b/CashRegisterTests/Test Input Files/EmptyFile.txt new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/CashRegisterTests/Test Input Files/EmptyFile.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/EmptyLineFile.txt b/CashRegisterTests/Test Input Files/EmptyLineFile.txt new file mode 100644 index 00000000..c6473c1e --- /dev/null +++ b/CashRegisterTests/Test Input Files/EmptyLineFile.txt @@ -0,0 +1,2 @@ + +5.55,6.66 \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/SingleTransactionTestFile.txt b/CashRegisterTests/Test Input Files/SingleTransactionTestFile.txt new file mode 100644 index 00000000..bfd78cb5 --- /dev/null +++ b/CashRegisterTests/Test Input Files/SingleTransactionTestFile.txt @@ -0,0 +1 @@ +2.50, 5.00 \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/TenderLessThanPrice.txt b/CashRegisterTests/Test Input Files/TenderLessThanPrice.txt new file mode 100644 index 00000000..6c37fe19 --- /dev/null +++ b/CashRegisterTests/Test Input Files/TenderLessThanPrice.txt @@ -0,0 +1 @@ +6.00, 4.50 \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/ValueTests/OverflowTransactionTestFile.txt b/CashRegisterTests/Test Input Files/ValueTests/OverflowTransactionTestFile.txt new file mode 100644 index 00000000..1ee76b03 --- /dev/null +++ b/CashRegisterTests/Test Input Files/ValueTests/OverflowTransactionTestFile.txt @@ -0,0 +1 @@ +7922816251426433759354395033511, 79228162514264337593543950335111 \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/ValueTests/PriceValueTestFile.txt b/CashRegisterTests/Test Input Files/ValueTests/PriceValueTestFile.txt new file mode 100644 index 00000000..cea39776 --- /dev/null +++ b/CashRegisterTests/Test Input Files/ValueTests/PriceValueTestFile.txt @@ -0,0 +1 @@ + 2.45,110.98 \ No newline at end of file diff --git a/CashRegisterTests/Test Input Files/ValueTests/TenderValueTestFile.txt b/CashRegisterTests/Test Input Files/ValueTests/TenderValueTestFile.txt new file mode 100644 index 00000000..cea39776 --- /dev/null +++ b/CashRegisterTests/Test Input Files/ValueTests/TenderValueTestFile.txt @@ -0,0 +1 @@ + 2.45,110.98 \ No newline at end of file