diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 00000000..a73cc8bc --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,25 @@ +name: .NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..32d3f45c --- /dev/null +++ b/.gitignore @@ -0,0 +1,408 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/csharp,dotnetcore +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,dotnetcore + + +## VSCode Files +.vscode + +### Csharp ### +## 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 + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# 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 +nunit-*.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/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# 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 +*.tlog +*.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 + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# 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 +# NuGet Symbol Packages +*.snupkg +# 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 + +# Nuget personal access tokens and Credentials +nuget.config + +# 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 +*.appxbundle +*.appxupload + +# 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 +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).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/ + +# 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 + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +### DotnetCore ### +# .NET Core build folders +bin/ +obj/ + +# Common node modules locations +/node_modules +/wwwroot/node_modules + +# End of https://www.toptal.com/developers/gitignore/api/csharp,dotnetcore \ No newline at end of file diff --git a/CashRegister.Tests/CashRegister.Tests.csproj b/CashRegister.Tests/CashRegister.Tests.csproj new file mode 100644 index 00000000..27d76a4a --- /dev/null +++ b/CashRegister.Tests/CashRegister.Tests.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CashRegister.Tests/Controllers/CashRegisterControllerTests.cs b/CashRegister.Tests/Controllers/CashRegisterControllerTests.cs new file mode 100644 index 00000000..f23eb898 --- /dev/null +++ b/CashRegister.Tests/Controllers/CashRegisterControllerTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using CashRegister.Controllers; +using CashRegister.Models; +using CashRegister.Services.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace CashRegister.Tests.Controllers +{ + [TestFixture] + public class CashRegisterControllerTests + { + private readonly Mock> mockLogger; + private Mock mockCsvParser; + private Mock mockChangeCalculator; + private Mock mockRandomChangeCalculator; + private CashRegisterController uut; + private Stream memoryStream = new MemoryStream(); + private Mock mockFormFile; + + private List expectedCashRegisterTransactions = new List() + { + new CashRegisterTransaction + { + costDue = 12.01m, + paid = 12.50m + }, + new CashRegisterTransaction + { + costDue = 3.33m, + paid = 17.50m + }, + new CashRegisterTransaction + { + costDue = 1.20m, + paid = 2.00m + } + }; + + public CashRegisterControllerTests() + { + + mockLogger = new Mock>(); + mockCsvParser = new Mock(); + mockChangeCalculator = new Mock(); + mockRandomChangeCalculator = new Mock(); + mockFormFile = new Mock(); + + uut = new CashRegisterController(mockLogger.Object, mockCsvParser.Object, mockChangeCalculator.Object, mockRandomChangeCalculator.Object); + } + + [SetUp] + public void SetUpTests() + { + mockFormFile.SetupGet(f => f.FileName).Returns("textData.csv"); + mockFormFile.Setup(f => f.OpenReadStream()) + .Returns(memoryStream); + + mockCsvParser.Setup(parser => parser.ParseCsvFile(It.IsAny())) + .Returns(expectedCashRegisterTransactions); + } + + [TearDown] + public void TearDownTests() + { + mockLogger.Reset(); + mockCsvParser.Reset(); + mockChangeCalculator.Reset(); + mockRandomChangeCalculator.Reset(); + mockFormFile.Reset(); + } + + + [Test] + public void CashRegisterController_Should_ReturnBadRequestOnNonValidFileTypes() + { + mockFormFile.SetupGet(f => f.FileName).Returns("textData.asdfadsf"); + + var response = uut.Index(mockFormFile.Object); + + Assert.IsInstanceOf(response); + } + + [Test] + public void CashRegisterController_Should_ReturnBadRequestIfFileIsNull() + { + var response = uut.Index(null); + + Assert.IsInstanceOf(response); + } + + [Test] + public void CashRegisterController_Should_ReturnCallParseCsv() + { + var memoryStream = new MemoryStream(); + mockFormFile.Setup(f => f.OpenReadStream()) + .Returns(memoryStream); + + var response = uut.Index(mockFormFile.Object); + + mockCsvParser.Verify(parser => parser.ParseCsvFile(memoryStream), Times.Once); + } + + [Test] + public void CashRegisterController_Should_CallChangeCalculatorMethodsOnce_AndRandomChangeCalculatorMethodsTwice() + { + var response = uut.Index(mockFormFile.Object); + + mockChangeCalculator.Verify(calc => calc.CalculateChange(It.IsAny(), It.IsAny()), Times.Once); + mockChangeCalculator.Verify(calc => calc.DetermineChange(It.IsAny()), Times.Once); + + mockRandomChangeCalculator.Verify(calc => calc.CalculateChange(It.IsAny(), It.IsAny()), Times.Exactly(2)); + mockRandomChangeCalculator.Verify(calc => calc.DetermineChange(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void CashRegisterController_Should_ReturnTheExpectedResponse() + { + var expectedTransactions = new List() + { + new CashRegisterTransaction + { + costDue = 12.01m, + paid = 12.50m + } + }; + + mockCsvParser.Setup(parser => parser.ParseCsvFile(It.IsAny())) + .Returns(expectedTransactions); + + var expectedResultString = "1 dollar, 3 quarters, 1 nickel"; + mockChangeCalculator.Setup(calc => calc.DetermineChange(It.IsAny())) + .Returns(expectedResultString); + + var response = uut.Index(mockFormFile.Object); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((OkObjectResult)response).StatusCode); + Assert.AreEqual(expectedResultString, ((OkObjectResult)response).Value); + } + + } +} \ No newline at end of file diff --git a/CashRegister.Tests/Services/ChangeCalculatorTests.cs b/CashRegister.Tests/Services/ChangeCalculatorTests.cs new file mode 100644 index 00000000..0be6fc1b --- /dev/null +++ b/CashRegister.Tests/Services/ChangeCalculatorTests.cs @@ -0,0 +1,112 @@ +using Moq; +using CashRegister.Exceptions; +using CashRegister.Services; +using NUnit.Framework; +using CashRegister.Services.Interfaces; + +namespace CashRegister.Tests.Services +{ + [TestFixture] + public class ChangeCalculatorTests + { + private Mock mockChangeBuilder; + protected ChangeCalculator uut; + + public ChangeCalculatorTests() + { + mockChangeBuilder = new Mock(); + uut = new ChangeCalculator(mockChangeBuilder.Object); + } + + [Test] + public void CalculateChange_Should_ReturnAdecimal() + { + decimal paid = 5.00m; + decimal cost = 1.66m; + var result = uut.CalculateChange(paid, cost); + Assert.IsInstanceOf(result); + } + + [Test] + [TestCase(75.00, 63.12, 11.88)] + [TestCase(5.00, 3.99, 1.01)] + [TestCase(100.67, 88.12, 12.55)] + public void CalculateChange_Should_ReturnExpectedResult( + decimal paid, + decimal cost, + decimal expected + ) + { + var result = uut.CalculateChange(paid, cost); + Assert.AreEqual(expected, result); + } + + [Test] + public void CalculateChange_ShouldThrow_IfCostIsANegativeValue() + { + var paid = -30; + var cost = 12; + var expectedExceptionMessage = "-30 is not able to be negative"; + + var thrownException = Assert.Throws(() => uut.CalculateChange(paid, cost)); + Assert.AreEqual(expectedExceptionMessage, thrownException.Message); + } + + [Test] + public void CalculateChange_ShouldThrow_IfPaidIsANegativeValue() + { + var paid = 30; + var cost = -12; + var expectedExceptionMessage = "-12 is not able to be negative"; + + var thrownException = Assert.Throws(() => uut.CalculateChange(paid, cost)); + Assert.AreEqual(expectedExceptionMessage, thrownException.Message); + } + + [Test] + public void DetermineChange_Should_ReturnAString() + { + var costDue = 3; + + mockChangeBuilder.Setup(cb => cb.BuildChangeString(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(""); + + var results = uut.DetermineChange(costDue); + Assert.IsInstanceOf(results); + } + + [Test] + public void DetermineChange_Should_CallChangeBuilderWithExpectedArguments() + { + var costDue = 2.67m; + var expectedDollars = 2; + var expectedQuarters = 2; + var expectedDimes = 1; + var expectedNickels = 1; + var expectedPennies = 2; + + var results = uut.DetermineChange(costDue); + + mockChangeBuilder.Verify(cb => cb.BuildChangeString(expectedDollars, expectedQuarters, expectedDimes, expectedNickels, expectedPennies), Times.Once); + } + + [Test] + public void DetermineChange_Should_ReturnTheExpectedResult() + { + var costDue = 2.67m; + var expectedDollars = 2; + var expectedQuarters = 2; + var expectedDimes = 1; + var expectedNickels = 1; + var expectedPennies = 2; + + var expectedResults = "2 dollars, 2 quarters, 1 dime, 1 nickel, 2 pennies"; + + mockChangeBuilder.Setup(cb => cb.BuildChangeString(expectedDollars, expectedQuarters, expectedDimes, expectedNickels, expectedPennies)) + .Returns(expectedResults); + + var results = uut.DetermineChange(costDue); + Assert.AreEqual(expectedResults, results); + } + } +} \ No newline at end of file diff --git a/CashRegister.Tests/Services/ChangeStringBuilderTests.cs b/CashRegister.Tests/Services/ChangeStringBuilderTests.cs new file mode 100644 index 00000000..7c3897a7 --- /dev/null +++ b/CashRegister.Tests/Services/ChangeStringBuilderTests.cs @@ -0,0 +1,38 @@ +using CashRegister.Services; +using NUnit.Framework; + +namespace CashRegister.Tests.Services +{ + [TestFixture] + public class ChangeStringBuilderTests + { + private ChangeStringBuilder uut; + + public ChangeStringBuilderTests() + { + uut = new ChangeStringBuilder(); + } + + [Test] + [TestCase(1, 1, 1, 1, 1, "1 dollar, 1 quarter, 1 dime, 1 nickel, 1 penny")] + [TestCase(2, 2, 2, 2, 2, "2 dollars, 2 quarters, 2 dimes, 2 nickels, 2 pennies")] + [TestCase(0, 1, 1, 1, 1, "1 quarter, 1 dime, 1 nickel, 1 penny")] + [TestCase(1, 0, 1, 1, 1, "1 dollar, 1 dime, 1 nickel, 1 penny")] + [TestCase(1, 1, 0, 1, 1, "1 dollar, 1 quarter, 1 nickel, 1 penny")] + [TestCase(1, 1, 1, 0, 0, "1 dollar, 1 quarter, 1 dime")] + [TestCase(0, 0, 0, 0, 0, "")] + public void ChangeStringBuilder_ShouldReturn_ExpectedStringWithIndividualResults( + decimal dollars, + decimal quarters, + decimal dimes, + decimal nickels, + decimal pennies, + string expectedString + ) + { + var result = uut.BuildChangeString(dollars, quarters, dimes, nickels, pennies); + + Assert.AreEqual(expectedString, result); + } + } +} \ No newline at end of file diff --git a/CashRegister.Tests/Services/CsvFileParserTest.cs b/CashRegister.Tests/Services/CsvFileParserTest.cs new file mode 100644 index 00000000..b2756dcc --- /dev/null +++ b/CashRegister.Tests/Services/CsvFileParserTest.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.IO; +using CashRegister.Models; +using CashRegister.Services; +using NUnit.Framework; + +namespace CashRegister.Tests.Services +{ + [TestFixture] + public class CsvFileParserTest + { + private CsvFileParser uut; + private Stream stream; + + public CsvFileParserTest() + { + uut = new CsvFileParser(); + } + + [SetUp] + public void SetUpTests() + { + stream = File.OpenRead(Directory.GetCurrentDirectory() + "/testData.txt"); + } + + [Test] + public void CsvParser_Should_ReturnTheExpectedListOfResults() + { + var expectedList = new List() + { + new CashRegisterTransaction() { costDue = 2.12m, paid = 3.00m}, + new CashRegisterTransaction() { costDue = 1.97m, paid = 2.00m}, + new CashRegisterTransaction() { costDue = 3.33m, paid = 5.00m}, + }; + + var results = uut.ParseCsvFile(stream); + + // Iterate through each item in the list and Assert it was found in our exppected results + results.ForEach(res => + { + var foundIdx = expectedList.FindIndex(transaction => transaction.costDue == res.costDue && transaction.paid == res.paid); + Assert.True(foundIdx > -1); + }); + } + + } +} \ No newline at end of file diff --git a/CashRegister.Tests/Services/RandomChangeCalculatorTest.cs b/CashRegister.Tests/Services/RandomChangeCalculatorTest.cs new file mode 100644 index 00000000..9145e7e8 --- /dev/null +++ b/CashRegister.Tests/Services/RandomChangeCalculatorTest.cs @@ -0,0 +1,125 @@ +using CashRegister.Exceptions; +using CashRegister.Services; +using CashRegister.Services.Interfaces; +using Moq; +using NUnit.Framework; + +namespace CashRegister.Tests.Services +{ + [TestFixture] + public class RandomChangeCalculatorTest + { + private Mock mockChangeStringBuilder; + private Mock mockRandomNumberGenerator; + private RandomChangeCalculator uut; + + public RandomChangeCalculatorTest() + { + mockChangeStringBuilder = new Mock(); + mockRandomNumberGenerator = new Mock(); + uut = new RandomChangeCalculator(mockChangeStringBuilder.Object, mockRandomNumberGenerator.Object); + } + + [SetUp] + public void BeforeEach() + { + mockRandomNumberGenerator.Setup(rng => rng.GenerateRandomInt(It.IsAny(), It.IsAny())).Returns(1); + mockChangeStringBuilder.Setup(cb => cb.BuildChangeString(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(""); + } + + [TearDown] + public void AfterEach() + { + mockChangeStringBuilder.Reset(); + mockRandomNumberGenerator.Reset(); + } + + [Test] + public void CalculateChange_Should_ReturnAdecimal() + { + decimal paid = 5.00m; + decimal cost = 1.66m; + var result = uut.CalculateChange(paid, cost); + Assert.IsInstanceOf(result); + } + + [Test] + [TestCase(75.00, 63.12, 11.88)] + [TestCase(5.00, 3.99, 1.01)] + [TestCase(100.67, 88.12, 12.55)] + public void CalculateChange_Should_ReturnExpectedResult( + decimal paid, + decimal cost, + decimal expected + ) + { + var result = uut.CalculateChange(paid, cost); + Assert.AreEqual(expected, result); + } + + [Test] + public void CalculateChange_ShouldThrow_IfCostIsANegativeValue() + { + var paid = -30; + var cost = 12; + var expectedExceptionMessage = "-30 is not able to be negative"; + + var thrownException = Assert.Throws(() => uut.CalculateChange(paid, cost)); + Assert.AreEqual(expectedExceptionMessage, thrownException.Message); + } + + [Test] + public void CalculateChange_ShouldThrow_IfPaidIsANegativeValue() + { + var paid = 30; + var cost = -12; + var expectedExceptionMessage = "-12 is not able to be negative"; + + var thrownException = Assert.Throws(() => uut.CalculateChange(paid, cost)); + Assert.AreEqual(expectedExceptionMessage, thrownException.Message); + } + + [Test] + public void DetermineChange_Should_ReturnAString() + { + var costDue = 3; + + var results = uut.DetermineChange(costDue); + Assert.IsInstanceOf(results); + } + + [Test] + public void DetermineChange_Should_CallRandomNumberGenerator5Times() + { + var changeDue = 1.66m; + var expectedRngCallCount = 4; + + uut.DetermineChange(changeDue); + mockRandomNumberGenerator.Verify(rng => rng.GenerateRandomInt(It.IsAny(), It.IsAny()), Times.Exactly(expectedRngCallCount)); + } + + [Test] + public void DetermineChange_Should_CallChangeStringBuilderWithExpectedArguments() + { + var changeDue = 1.66m; + var expectedDollars = 1; + var expectedQuarter = 1; + var expectedDimes = 1; + var expectedNickes = 1; + var expectedPennies = 26; + + uut.DetermineChange(changeDue); + + mockChangeStringBuilder.Verify(cb => + cb.BuildChangeString( + expectedDollars, + expectedQuarter, + expectedDimes, + expectedNickes, + expectedPennies + ), Times.Once); + } + + } +} \ No newline at end of file diff --git a/CashRegister.Tests/Services/RandomNumberGeneratorTests.cs b/CashRegister.Tests/Services/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..e06beef7 --- /dev/null +++ b/CashRegister.Tests/Services/RandomNumberGeneratorTests.cs @@ -0,0 +1,24 @@ +using CashRegister.Services; +using NUnit.Framework; + +namespace CashRegister.Tests.Services +{ + [TestFixture] + public class RandomNumberGeneratorTests + { + public RandomNumberGenerator uut; + + public RandomNumberGeneratorTests() + { + uut = new RandomNumberGenerator(); + } + + [Test] + public void RandomNumberGenerator_Should_ReturnAnInteger() + { + var result = uut.GenerateRandomInt(0, 10); + + Assert.IsInstanceOf(result); + } + } +} \ No newline at end of file diff --git a/CashRegister.Tests/testData.txt b/CashRegister.Tests/testData.txt new file mode 100644 index 00000000..fba6116f --- /dev/null +++ b/CashRegister.Tests/testData.txt @@ -0,0 +1,3 @@ +2.12,3.00 +1.97,2.00 +3.33,5.00 \ No newline at end of file diff --git a/CashRegister.sln b/CashRegister.sln new file mode 100644 index 00000000..66f5e237 --- /dev/null +++ b/CashRegister.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CashRegister", "CashRegister\CashRegister.csproj", "{20DA9783-DCB5-47C8-82D9-CCB97C069F68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CashRegister.Tests", "CashRegister.Tests\CashRegister.Tests.csproj", "{C437D319-C020-4051-A049-A07F46F9CAE9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|x64.ActiveCfg = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|x64.Build.0 = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|x86.ActiveCfg = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Debug|x86.Build.0 = Debug|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|Any CPU.Build.0 = Release|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|x64.ActiveCfg = Release|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|x64.Build.0 = Release|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|x86.ActiveCfg = Release|Any CPU + {20DA9783-DCB5-47C8-82D9-CCB97C069F68}.Release|x86.Build.0 = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|x64.ActiveCfg = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|x64.Build.0 = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|x86.ActiveCfg = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Debug|x86.Build.0 = Debug|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|Any CPU.Build.0 = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|x64.ActiveCfg = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|x64.Build.0 = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|x86.ActiveCfg = Release|Any CPU + {C437D319-C020-4051-A049-A07F46F9CAE9}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/CashRegister/CashRegister.csproj b/CashRegister/CashRegister.csproj new file mode 100644 index 00000000..6001141f --- /dev/null +++ b/CashRegister/CashRegister.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + + diff --git a/CashRegister/Constants/Denominations.cs b/CashRegister/Constants/Denominations.cs new file mode 100644 index 00000000..123b63b9 --- /dev/null +++ b/CashRegister/Constants/Denominations.cs @@ -0,0 +1,11 @@ +namespace CashRegister.Constants +{ + public class Denomination + { + public static int PENNY = 1; + public static int NICKEL = 5; + public static int DIME = 10; + public static int QUARTER = 25; + public static int DOLLAR = 100; + } +} \ No newline at end of file diff --git a/CashRegister/Controllers/CashRegisterController.cs b/CashRegister/Controllers/CashRegisterController.cs new file mode 100644 index 00000000..6e16d6ec --- /dev/null +++ b/CashRegister/Controllers/CashRegisterController.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using CashRegister.Services.Interfaces; +using System.Linq; +using System.IO; +using System; + +namespace CashRegister.Controllers +{ + [Route("/api/v1/CashRegister")] + public class CashRegisterController : Controller + { + private readonly ILogger logger; + private ICsvFileParser csvParser; + private IChangeCalculator changeCalculator; + private IRandomChangeCalculator randomChangeCalculator; + + private readonly List acceptedFileTypes = new List() + { + ".txt", + ".csv" + }; + + public CashRegisterController( + ILogger logger, + ICsvFileParser csvParser, + IChangeCalculator changeCalculator, + IRandomChangeCalculator randomChangeCalculator + ) + { + this.logger = logger; + this.csvParser = csvParser; + this.changeCalculator = changeCalculator; + this.randomChangeCalculator = randomChangeCalculator; + } + + [HttpPost] + public IActionResult Index(IFormFile file) + { + if (file == null || !acceptedFileTypes.Contains(Path.GetExtension(file.FileName).ToLower())) + { + return BadRequest("Please submit a file with a .txt or .csv extension"); + } + + try + { + var stream = file.OpenReadStream(); + var results = csvParser.ParseCsvFile(stream); + + var changeStrings = results.Select(res => + { + var changeDue = 0m; + + // If the cost in cents is divisible by 3, the client wants to + // use the random number generator to generate the change + if (res.costDue * 100 % 3 == 0) + { + changeDue = randomChangeCalculator.CalculateChange(res.paid, res.costDue); + + return randomChangeCalculator.DetermineChange(changeDue); + } + + changeDue = changeCalculator.CalculateChange(res.paid, res.costDue); + + return changeCalculator.DetermineChange(changeDue); + }).ToList(); + + return Ok(string.Join("\r\n", changeStrings)); + + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + return BadRequest($"An error occured with the following message: {ex.Message}"); + } + } + + } +} diff --git a/CashRegister/Exceptions/IllegalNegativeException.cs b/CashRegister/Exceptions/IllegalNegativeException.cs new file mode 100644 index 00000000..327025d9 --- /dev/null +++ b/CashRegister/Exceptions/IllegalNegativeException.cs @@ -0,0 +1,9 @@ +using System; + +namespace CashRegister.Exceptions +{ + public class IllegalNegativeException : Exception + { + public IllegalNegativeException(decimal value) : base($"{value} is not able to be negative") { } + } +} \ No newline at end of file diff --git a/CashRegister/Models/CashRegisterTransaction.cs b/CashRegister/Models/CashRegisterTransaction.cs new file mode 100644 index 00000000..b471890a --- /dev/null +++ b/CashRegister/Models/CashRegisterTransaction.cs @@ -0,0 +1,9 @@ +namespace CashRegister.Models +{ + public class CashRegisterTransaction + { + public decimal costDue { get; set; } + public decimal paid { get; set; } + + } +} \ No newline at end of file diff --git a/CashRegister/Program.cs b/CashRegister/Program.cs new file mode 100644 index 00000000..2ed35f8e --- /dev/null +++ b/CashRegister/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CashRegister +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/CashRegister/Properties/launchSettings.json b/CashRegister/Properties/launchSettings.json new file mode 100644 index 00000000..a02f2b2b --- /dev/null +++ b/CashRegister/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7179", + "sslPort": 44324 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CashRegister": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CashRegister/Services/ChangeCalculator.cs b/CashRegister/Services/ChangeCalculator.cs new file mode 100644 index 00000000..614f5c7e --- /dev/null +++ b/CashRegister/Services/ChangeCalculator.cs @@ -0,0 +1,71 @@ +using System; +using CashRegister.Constants; +using CashRegister.Exceptions; +using CashRegister.Services.Interfaces; + +namespace CashRegister.Services +{ + public class ChangeCalculator : IChangeCalculator + { + private IChangeStringBuilder changeStringBuilder; + + public ChangeCalculator( + IChangeStringBuilder changeBuilder + ) + { + this.changeStringBuilder = changeBuilder; + } + + public decimal CalculateChange(decimal paid, decimal cost) + { + if (paid < 0) + { + throw new IllegalNegativeException(paid); + } + + if (cost < 0) + { + throw new IllegalNegativeException(cost); + } + return paid - cost; + } + + public string DetermineChange(decimal changeDue) + { + // Multiply * 100 + changeDue = changeDue * 100; + + decimal dollars; + decimal quarters; + decimal dimes; + decimal nickels; + decimal pennies; + + dollars = determineDenomination(changeDue, Denomination.DOLLAR); + changeDue = RecalculateChangeDue(changeDue, dollars, Denomination.DOLLAR); + + quarters = determineDenomination(changeDue, Denomination.QUARTER); + changeDue = RecalculateChangeDue(changeDue, quarters, Denomination.QUARTER); + + dimes = determineDenomination(changeDue, Denomination.DIME); + changeDue = RecalculateChangeDue(changeDue, dimes, Denomination.DIME); + + nickels = determineDenomination(changeDue, Denomination.NICKEL); + changeDue = RecalculateChangeDue(changeDue, nickels, Denomination.NICKEL); + + pennies = changeDue; + + return changeStringBuilder.BuildChangeString(dollars, quarters, dimes, nickels, pennies); + } + + private decimal determineDenomination(decimal changeDue, int denomination) + { + return Math.Floor(changeDue / denomination); + } + + private decimal RecalculateChangeDue(decimal changeDue, decimal denominationCount, int denomination) + { + return changeDue - (denominationCount * denomination); + } + } +} \ No newline at end of file diff --git a/CashRegister/Services/ChangeStringBuilder.cs b/CashRegister/Services/ChangeStringBuilder.cs new file mode 100644 index 00000000..324f9393 --- /dev/null +++ b/CashRegister/Services/ChangeStringBuilder.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using CashRegister.Services.Interfaces; + +namespace CashRegister.Services +{ + public class ChangeStringBuilder : IChangeStringBuilder + { + public string BuildChangeString(decimal dollars, decimal quarters, decimal dimes, decimal nickels, decimal pennies) + { + var dollarsLabel = dollars == 1 ? "dollar" : "dollars"; + var quartersLabel = quarters == 1 ? "quarter" : "quarters"; + var dimesLabel = dimes == 1 ? "dime" : "dimes"; + var nickelsLabel = nickels == 1 ? "nickel" : "nickels"; + var penniesLabel = pennies == 1 ? "penny" : "pennies"; + + StringBuilder sb = new StringBuilder(); + List strings = new List(); + + AppendString(strings, dollars, dollarsLabel); + AppendString(strings, quarters, quartersLabel); + AppendString(strings, dimes, dimesLabel); + AppendString(strings, nickels, nickelsLabel); + AppendString(strings, pennies, penniesLabel); + + return sb.AppendJoin(", ", strings).ToString(); + } + + private void AppendString(List strings, decimal val, string label) + { + var numberFormat = new NumberFormatInfo() + { + NumberDecimalDigits = 0 + }; + + if (val != 0) + { + strings.Add($"{((int) val).ToString(numberFormat)} {label}"); + } + + } + } +} \ No newline at end of file diff --git a/CashRegister/Services/CsvFileParser.cs b/CashRegister/Services/CsvFileParser.cs new file mode 100644 index 00000000..e454d311 --- /dev/null +++ b/CashRegister/Services/CsvFileParser.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using System.IO; +using CashRegister.Models; +using CashRegister.Services.Interfaces; +using CsvHelper; +using CsvHelper.Configuration; + +namespace CashRegister.Services +{ + public class CsvFileParser : ICsvFileParser + { + public CsvFileParser() + { + + } + + public List ParseCsvFile(Stream stream) + { + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + IgnoreBlankLines = true + }; + + List cashRegisterTransactions; + + using (var csvParser = new CsvReader(new StreamReader(stream), config)) + { + cashRegisterTransactions = csvParser.GetRecords().ToList(); + } + + return cashRegisterTransactions; + } + } +} \ No newline at end of file diff --git a/CashRegister/Services/Interfaces/IChangeCalculator.cs b/CashRegister/Services/Interfaces/IChangeCalculator.cs new file mode 100644 index 00000000..bdb56a8a --- /dev/null +++ b/CashRegister/Services/Interfaces/IChangeCalculator.cs @@ -0,0 +1,8 @@ +namespace CashRegister.Services.Interfaces +{ + public interface IChangeCalculator + { + public decimal CalculateChange(decimal paid, decimal cost); + public string DetermineChange(decimal changeDue); + } +} \ No newline at end of file diff --git a/CashRegister/Services/Interfaces/IChangeStringBuilder.cs b/CashRegister/Services/Interfaces/IChangeStringBuilder.cs new file mode 100644 index 00000000..a5ca9ab2 --- /dev/null +++ b/CashRegister/Services/Interfaces/IChangeStringBuilder.cs @@ -0,0 +1,7 @@ +namespace CashRegister.Services.Interfaces +{ + public interface IChangeStringBuilder + { + string BuildChangeString(decimal dollars, decimal quarters, decimal dimes, decimal nickels, decimal pennies); + } +} \ No newline at end of file diff --git a/CashRegister/Services/Interfaces/ICsvFileParser.cs b/CashRegister/Services/Interfaces/ICsvFileParser.cs new file mode 100644 index 00000000..105a9282 --- /dev/null +++ b/CashRegister/Services/Interfaces/ICsvFileParser.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.IO; +using CashRegister.Models; + +namespace CashRegister.Services.Interfaces +{ + public interface ICsvFileParser + { + List ParseCsvFile(Stream stream); + } +} \ No newline at end of file diff --git a/CashRegister/Services/Interfaces/IRandomChangeCalculator.cs b/CashRegister/Services/Interfaces/IRandomChangeCalculator.cs new file mode 100644 index 00000000..f2ebed23 --- /dev/null +++ b/CashRegister/Services/Interfaces/IRandomChangeCalculator.cs @@ -0,0 +1,7 @@ +namespace CashRegister.Services.Interfaces +{ + public interface IRandomChangeCalculator : IChangeCalculator + { + + } +} \ No newline at end of file diff --git a/CashRegister/Services/Interfaces/IRandomNumberGenerator.cs b/CashRegister/Services/Interfaces/IRandomNumberGenerator.cs new file mode 100644 index 00000000..79a2d42f --- /dev/null +++ b/CashRegister/Services/Interfaces/IRandomNumberGenerator.cs @@ -0,0 +1,7 @@ +namespace CashRegister.Services.Interfaces +{ + public interface IRandomNumberGenerator + { + int GenerateRandomInt(int minValue, int maxValue); + } +} \ No newline at end of file diff --git a/CashRegister/Services/RandomChangeCalculator.cs b/CashRegister/Services/RandomChangeCalculator.cs new file mode 100644 index 00000000..403b332d --- /dev/null +++ b/CashRegister/Services/RandomChangeCalculator.cs @@ -0,0 +1,73 @@ +using System; +using CashRegister.Constants; +using CashRegister.Exceptions; +using CashRegister.Services.Interfaces; + +namespace CashRegister.Services +{ + public class RandomChangeCalculator : IRandomChangeCalculator + { + private IChangeStringBuilder changeStringBuilder; + private IRandomNumberGenerator randomNumberGenerator; + + public RandomChangeCalculator( + IChangeStringBuilder changeStringBuilder, + IRandomNumberGenerator randomNumberGenerator) + { + this.changeStringBuilder = changeStringBuilder; + this.randomNumberGenerator = randomNumberGenerator; + } + + public decimal CalculateChange(decimal paid, decimal cost) + { + if (paid < 0) + { + throw new IllegalNegativeException(paid); + } + + if (cost < 0) + { + throw new IllegalNegativeException(cost); + } + return paid - cost; + } + + public string DetermineChange(decimal changeDue) + { + // Multiply * 100 + changeDue = changeDue * 100; + + decimal dollars; + decimal quarters; + decimal dimes; + decimal nickels; + decimal pennies; + + dollars = DetermineDenomination(changeDue, Denomination.DOLLAR); + changeDue = RecalculateChangeDue(changeDue, dollars, Denomination.DOLLAR); + + quarters = DetermineDenomination(changeDue, Denomination.QUARTER); + changeDue = RecalculateChangeDue(changeDue, quarters, Denomination.QUARTER); + + dimes = DetermineDenomination(changeDue, Denomination.DIME); + changeDue = RecalculateChangeDue(changeDue, dimes, Denomination.DIME); + + nickels = DetermineDenomination(changeDue, Denomination.NICKEL); + changeDue = RecalculateChangeDue(changeDue, nickels, Denomination.NICKEL); + + pennies = (int) changeDue; + + return changeStringBuilder.BuildChangeString(dollars, quarters, dimes, nickels, pennies); + } + + private decimal DetermineDenomination(decimal changeDue, int denomination) + { + return randomNumberGenerator.GenerateRandomInt(0, (int) Math.Floor(changeDue / denomination)); + } + + private decimal RecalculateChangeDue(decimal changeDue, decimal denominationCount, int denomination) + { + return changeDue - (denominationCount * denomination); + } + } +} \ No newline at end of file diff --git a/CashRegister/Services/RandomNumberGenerator.cs b/CashRegister/Services/RandomNumberGenerator.cs new file mode 100644 index 00000000..469e3c2d --- /dev/null +++ b/CashRegister/Services/RandomNumberGenerator.cs @@ -0,0 +1,16 @@ +using System; +using CashRegister.Services.Interfaces; + +namespace CashRegister.Services +{ + public class RandomNumberGenerator : IRandomNumberGenerator + { + public int GenerateRandomInt(int minValue, int maxValue) + { + Random rand = new Random(); + + // +1 since maxValue for method is not inclusive + return rand.Next(minValue, maxValue + 1); + } + } +} \ No newline at end of file diff --git a/CashRegister/Startup.cs b/CashRegister/Startup.cs new file mode 100644 index 00000000..71088c11 --- /dev/null +++ b/CashRegister/Startup.cs @@ -0,0 +1,71 @@ +using CashRegister.Services; +using CashRegister.Services.Interfaces; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace CashRegister +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddControllersWithViews(); + + services.AddSwaggerGen(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + app.UseSwagger(); + + app.UseSwaggerUI(config => + { + config.SwaggerEndpoint("/swagger/v1/swagger.json", "Cash Register API"); + }); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/CashRegister/appsettings.Development.json b/CashRegister/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/CashRegister/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/CashRegister/appsettings.json b/CashRegister/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/CashRegister/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/ORIGINAL_PROMPT.md b/ORIGINAL_PROMPT.md new file mode 100644 index 00000000..62a96fc3 --- /dev/null +++ b/ORIGINAL_PROMPT.md @@ -0,0 +1,42 @@ +Cash Register +============ + +The Problem +----------- +Creative Cash Draw Solutions is a client who wants to provide something different for the cashiers who use their system. The function of the application is to tell the cashier how much change is owed and what denominations should be used. In most cases the app should return the minimum amount of physical change, but the client would like to add a twist. If the total due in cents is divisible by 3, the app should randomly generate the change denominations (but the math still needs to be right :)) + +Please write a program which accomplishes the clients goals. The program should: + +1. Accept a flat file as input + 1. Each line will contain the total due and the amount paid separated by a comma (for example: 2.13,3.00) + 2. Expect that there will be multiple lines +2. Output the change the cashier should return to the customer + 1. The return string should look like: 1 dollar,2 quarters,1 nickel, etc ... + 2. Each new line in the input file should be a new line in the output file + +Sample Input +------------ +2.12,3.00 + +1.97,2.00 + +3.33,5.00 + +Sample Output +------------- +3 quarters,1 dime,3 pennies + +3 pennies + +1 dollar,1 quarter,6 nickels,12 pennies + +*Remember the last one is random + +Additional Information +--------------------- +This exercise is used to help us get a better picture of how you approach and solve for a given problem. Your submission will be evaluated based on a variety of criteria including, but not limited to, product quality, demonstrated knowledge of system design and coding best practices, completeness, and ease of use from a consumer and engineering teammate perspective. The completed solution should give us a good picture of your abilities and style, so feel free to use the programming language and tools with which you are most comfortable. + +Prior to submission, please fork this repository. If your solution includes code auto generated by a development tool, please use an additional commit to clearly separate it from your own work. When you have completed your solution, please issue a pull request to notify us that you are ready. + +Have fun! + diff --git a/README.md b/README.md index 62a96fc3..cf140ec6 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,36 @@ -Cash Register -============ +# Prerequisites -The Problem ------------ -Creative Cash Draw Solutions is a client who wants to provide something different for the cashiers who use their system. The function of the application is to tell the cashier how much change is owed and what denominations should be used. In most cases the app should return the minimum amount of physical change, but the client would like to add a twist. If the total due in cents is divisible by 3, the app should randomly generate the change denominations (but the math still needs to be right :)) +1. [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0) +- This may require a restart +- The MVC project was created using the `dotnet new mvc` cli command. +- The test project was created using the `dotnet new nunit` cli command -Please write a program which accomplishes the clients goals. The program should: +## Running the Application +Using the `dotnet` command line interface, navigate to [the CashRegister project folder](./CashRegister) and run the following command: -1. Accept a flat file as input - 1. Each line will contain the total due and the amount paid separated by a comma (for example: 2.13,3.00) - 2. Expect that there will be multiple lines -2. Output the change the cashier should return to the customer - 1. The return string should look like: 1 dollar,2 quarters,1 nickel, etc ... - 2. Each new line in the input file should be a new line in the output file +``` +dotnet run +``` -Sample Input ------------- -2.12,3.00 +### Swagger UI +After running this command, you can navigate to the port specified in the output of the above command. To navigate to the Swagger UI, +simply append `/swagger` to your localhost port e.g: `https://localhost:5001/swagger` -1.97,2.00 +## Testing the Application -3.33,5.00 +### Unit Testing +From the root of the project run the following command to run the unit tests: -Sample Output -------------- -3 quarters,1 dime,3 pennies +``` +dotnet test +``` -3 pennies +### Manual Testing +The application can be manually tested by navigating to the [Swagger link.](#swagger-ui) This provides a physical and easy way to +verify the code. -1 dollar,1 quarter,6 nickels,12 pennies - -*Remember the last one is random - -Additional Information ---------------------- -This exercise is used to help us get a better picture of how you approach and solve for a given problem. Your submission will be evaluated based on a variety of criteria including, but not limited to, product quality, demonstrated knowledge of system design and coding best practices, completeness, and ease of use from a consumer and engineering teammate perspective. The completed solution should give us a good picture of your abilities and style, so feel free to use the programming language and tools with which you are most comfortable. - -Prior to submission, please fork this repository. If your solution includes code auto generated by a development tool, please use an additional commit to clearly separate it from your own work. When you have completed your solution, please issue a pull request to notify us that you are ready. - -Have fun! +# Technologies +1. .NET 5 - MVC Framework +2. [SwashBuckle](https://www.nuget.org/packages/Swashbuckle) - API Documentation Generation +3. [CsvHelper](https://www.nuget.org/packages/CsvHelper) - Library for parsing CSV related data \ No newline at end of file