diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a72f3ddc51b9d2431412fae820a6c7a9610a8c8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,454 @@ +## 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 +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# 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 +*.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 + +# 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 + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..29a14c16a0e9e631aa6c8f3249cdb70a4ccc5477 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/homework-2-test-first-development/bin/Debug/net6.0/homework-2-test-first-development.dll", + "args": [], + "cwd": "${workspaceFolder}/homework-2-test-first-development", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000000000000000000000000000000000..6082ae14c2deeac4f071f8de83678369a6e9c1a2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/homework-2-test-first-development/homework-2-test-first-development.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/homework-2-test-first-development/homework-2-test-first-development.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/homework-2-test-first-development/homework-2-test-first-development.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/homework-2-test-first-development.sln b/homework-2-test-first-development.sln new file mode 100644 index 0000000000000000000000000000000000000000..6369438d59979a93153be044180810f95f15552a --- /dev/null +++ b/homework-2-test-first-development.sln @@ -0,0 +1,28 @@ + +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}") = "homework-2-test-first-development", "homework-2-test-first-development\homework-2-test-first-development.csproj", "{F9AEC99A-E67B-44EB-9CD9-92DC0DD47AB0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "homework-2-test-first-development.test", "homework-2-test-first-development.test\homework-2-test-first-development.test.csproj", "{05E1FD50-7888-4A6B-B3C6-5160937F846F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F9AEC99A-E67B-44EB-9CD9-92DC0DD47AB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9AEC99A-E67B-44EB-9CD9-92DC0DD47AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9AEC99A-E67B-44EB-9CD9-92DC0DD47AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9AEC99A-E67B-44EB-9CD9-92DC0DD47AB0}.Release|Any CPU.Build.0 = Release|Any CPU + {05E1FD50-7888-4A6B-B3C6-5160937F846F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05E1FD50-7888-4A6B-B3C6-5160937F846F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05E1FD50-7888-4A6B-B3C6-5160937F846F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05E1FD50-7888-4A6B-B3C6-5160937F846F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/homework-2-test-first-development.test/BarcodeProcessorTest.cs b/homework-2-test-first-development.test/BarcodeProcessorTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..61aaa91a54da52d8582998ad397b5b3fd36f14bf --- /dev/null +++ b/homework-2-test-first-development.test/BarcodeProcessorTest.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Services; + +namespace homework_2_test_first_development.test; + +public class BarcodeProcessorTest +{ + [Fact] + public void Should_Parse_Successfully_When_Barcodes_Are_Regular() + { + // Given + IEnumerable selectedBarcodes = new string[] + { + "ITEM000001", + "ITEM000001", + "ITEM000001", + "ITEM000003-2" + }; + + // When + var orderItems = BarcodeProcessor.ParseBarcodes(selectedBarcodes); + + // Then + orderItems.Should().HaveCount(4) + .And.Contain(o => o.Barcode == "ITEM000001") + .And.Contain(o => o.Barcode == "ITEM000003"); + } + + [Fact] + public void Should_Group_Successfully_When_Barcodes_Are_Regular() + { + // Given + IEnumerable barcodesAndAmount = new OrderItem[] + { + new OrderItem {Barcode = "ITEM000001", Amount = 1}, + new OrderItem {Barcode = "ITEM000001", Amount = 1}, + new OrderItem {Barcode = "ITEM000001", Amount = 1}, + new OrderItem {Barcode = "ITEM000003", Amount = 2}, + }; + + // When + var groupedBarcodes = BarcodeProcessor.GroupBarcodes(barcodesAndAmount); + + // Then + groupedBarcodes.Should().HaveCount(2).And.BeEquivalentTo(new OrderItem[] { + new OrderItem { Barcode = "ITEM000001", Amount = 3}, + new OrderItem { Barcode = "ITEM000003", Amount = 2}, + }); + } +} diff --git a/homework-2-test-first-development.test/BarcodeValidatorTest.cs b/homework-2-test-first-development.test/BarcodeValidatorTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..a900e19224699ba4a16f46d5b1f8263648c54eaa --- /dev/null +++ b/homework-2-test-first-development.test/BarcodeValidatorTest.cs @@ -0,0 +1,137 @@ +using AutoFixture; +using FluentAssertions; +using homework_2_test_first_development.Enums; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Validators; + +namespace homework_2_test_first_development.test; + +public class BarcodeValidatorTest +{ + private readonly Fixture _fixture = new(); + private static string BarcodePrefix = "ITEM"; + + public BarcodeValidatorTest() + { + _fixture.Customize(c => c.FromFactory( + (string name, decimal price, int id) => new Product { Name = name, Price = price, Barcode = $"{BarcodePrefix}{id:D6}"}) + .OmitAutoProperties()); + } + + [Fact] + public void Should_Return_Error_Invalid_Barcode_When_Barcode_Contains_Special_Characters() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "(ITEM000001)"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.InvalidBarcode); + } + + [Fact] + public void Should_Return_Error_Invalid_Barcode_When_Amount_Contains_Special_Characters() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "ITEM000003-2A"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.InvalidBarcode); + } + + [Fact] + public void Should_Return_Error_Invalid_Barcode_When_Barcode_Is_Empty() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = string.Empty; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.InvalidBarcode); + } + + [Fact] + public void Should_Return_Error_Invalid_Barcode_When_Barcode_Is_Empty_But_Amount_Not_Empty() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "-3"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.InvalidBarcode); + } + + [Fact] + public void Should_Return_Error_Invalid_Barcode_When_Amount_Larger_Than_Maximum() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "ITEM000004-100"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.InvalidBarcode); + } + + [Fact] + public void Should_Return_Error_Product_Not_Found_When_Product_Not_Found() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "ITEM0000001-2"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.ProductNotFound); + } + + [Fact] + public void Should_Return_Valid_When_Everything_Is_OK() + { + // Given + var products = _fixture.CreateMany(); + var selectedBarcodes = products.Select(p => p.Barcode); + + // When + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + // Then + validationResult.Should().BeOfType().Which + .Error.Should().Be(ErrorType.Valid); + } +} \ No newline at end of file diff --git a/homework-2-test-first-development.test/ReceiptCreatorTest.cs b/homework-2-test-first-development.test/ReceiptCreatorTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..35b2ca525accd665b02b760c1a971686f3fff759 --- /dev/null +++ b/homework-2-test-first-development.test/ReceiptCreatorTest.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Services; + +namespace homework_2_test_first_development.test; + +public class ReceiptCreatorTest +{ + public ReceiptCreatorTest() + { + } + + [Fact] + public void Should_Create_Receipt_Successfully() + { + // Given + var products = new Product[] + { + new Product { Barcode = "ITEM000001", Name = "Apple", Price = 1.0m}, + new Product { Barcode = "ITEM000002", Name = "Orange", Price = 2.0m}, + new Product { Barcode = "ITEM000003", Name = "Pear", Price = 3.0m}, + }; + + var groupedBarcodes = new OrderItem[] + { + new OrderItem { Barcode = "ITEM000001", Amount = 1}, + new OrderItem { Barcode = "ITEM000002", Amount = 2}, + new OrderItem { Barcode = "ITEM000003", Amount = 3}, + }; + + // When + var receipt = ReceiptCreator.CreateReceipt(groupedBarcodes, products); + + // Then + receipt.TotalPrice.Should().Be(1 * 1 + 2 * 2 + 3 * 3); + receipt.Products.Should().HaveCount(3); + } +} diff --git a/homework-2-test-first-development.test/ReceiptFormatterTest.cs b/homework-2-test-first-development.test/ReceiptFormatterTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..4694294bd85a0232f85e7e380e48d5ed00539b00 --- /dev/null +++ b/homework-2-test-first-development.test/ReceiptFormatterTest.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Services; + +namespace homework_2_test_first_development.test; + +public class ReceiptFormatterTest +{ + [Fact] + public void Should_Return_Formatted_Receipt_Content_Successfully() + { + // Given + Receipt receipt = new Receipt() + { + Products = new List + { + new OrderItem { Name = "Cola", Amount = 3, Price = 3, TotalPrice = 9}, + new OrderItem { Name = "Sprit", Amount = 2, Price = 3, TotalPrice = 6}, + }, + TotalPrice = 15 + }; + + // When + var receiptContent = ReceiptFormatter.FormatReceipt(receipt); + + // Then + + receiptContent.Should().Be(@"Receipt +------- +Name: Cola, Amount: 3, Price: 3.00, Total: 9.00 +Name: Sprit, Amount: 2, Price: 3.00, Total: 6.00 +------- +Total: 15.00"); + + } +} diff --git a/homework-2-test-first-development.test/ReceiptPrinterTest.cs b/homework-2-test-first-development.test/ReceiptPrinterTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..a092df48230d724786996b76739ee3bd67c1a40f --- /dev/null +++ b/homework-2-test-first-development.test/ReceiptPrinterTest.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Services; +using Xunit; + +namespace homework_2_test_first_development.test; + +public class ReceiptPrinterTest +{ + private readonly Fixture _fixture = new(); + private static string BarcodePrefix = "ITEM"; + + public ReceiptPrinterTest() + { + _fixture.Customize(c => c.FromFactory( + (string name, decimal price, int id) => new Product { Name = name, Price = price, Barcode = $"{BarcodePrefix}{id:D6}" }) + .OmitAutoProperties()); + } + + [Fact] + public void Should_Print_Not_Recognized_When_Barcodes_Format_Error() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "(ITEM000001)"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var receiptContent = ReceiptPrinter.PrintReceipt(selectedBarcodes, products); + + // Then + receiptContent.Should().Contain("ERROR").And.Contain("recognized"); + } + + [Fact] + public void Should_Print_Product_Not_Found_When_Barcode_Not_Found() + { + // Given + var products = _fixture.CreateMany(); + var invalidBarcode = "ITEM0000001"; + + var selectedBarcodes = products.Select(p => p.Barcode).Append(invalidBarcode); + + // When + var receiptContent = ReceiptPrinter.PrintReceipt(selectedBarcodes, products); + + // Then + receiptContent.Should().Contain("ERROR").And.Contain(invalidBarcode); + } + + [Fact] + public void Should_Print_Receipt_Content_When_Everything_Is_OK() + { + // Given + var products = new Product[] + { + new Product { Barcode = "ITEM000001", Name = "Cola", Price = 3.0m}, + new Product { Barcode = "ITEM000003", Name = "Sprit", Price = 3.0m}, + }; + + string[] selectedBarcodes = + { + "ITEM000001", + "ITEM000001", + "ITEM000001", + "ITEM000003-2" + }; + + // When + var receiptContent = ReceiptPrinter.PrintReceipt(selectedBarcodes, products); + + // Then + receiptContent.Should().Be(@"Receipt +------- +Name: Cola, Amount: 3, Price: 3.00, Total: 9.00 +Name: Sprit, Amount: 2, Price: 3.00, Total: 6.00 +------- +Total: 15.00"); + } +} diff --git a/homework-2-test-first-development.test/Usings.cs b/homework-2-test-first-development.test/Usings.cs new file mode 100644 index 0000000000000000000000000000000000000000..8c927eb747a6a304db265e6753aee6c47504f604 --- /dev/null +++ b/homework-2-test-first-development.test/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/homework-2-test-first-development.test/homework-2-test-first-development.test.csproj b/homework-2-test-first-development.test/homework-2-test-first-development.test.csproj new file mode 100644 index 0000000000000000000000000000000000000000..9b6ab6f0c5122c6ecaa38a20c908a5afeb6eb18b --- /dev/null +++ b/homework-2-test-first-development.test/homework-2-test-first-development.test.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + homework_2_test_first_development.test + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/homework-2-test-first-development/AssemblyInfo.cs b/homework-2-test-first-development/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..98067a44eb450f7c2a55eb55fd4c768e8c6fb89b --- /dev/null +++ b/homework-2-test-first-development/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("homework-2-test-first-development.test")] \ No newline at end of file diff --git a/homework-2-test-first-development/Enums/ErrorType.cs b/homework-2-test-first-development/Enums/ErrorType.cs new file mode 100644 index 0000000000000000000000000000000000000000..a50ff5a64c5f1db12bf1de1c2a369f1439368007 --- /dev/null +++ b/homework-2-test-first-development/Enums/ErrorType.cs @@ -0,0 +1,8 @@ +namespace homework_2_test_first_development.Enums; + +internal enum ErrorType +{ + Valid, + InvalidBarcode, + ProductNotFound +} diff --git a/homework-2-test-first-development/Exceptions/ProductNotFoundException.cs b/homework-2-test-first-development/Exceptions/ProductNotFoundException.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ded54ce1e9f7950088966ca187cfe9e50e2b2a5 --- /dev/null +++ b/homework-2-test-first-development/Exceptions/ProductNotFoundException.cs @@ -0,0 +1,5 @@ +namespace homework_2_test_first_development.Exceptions; + +public class ProductNotFoundException : Exception +{ +} diff --git a/homework-2-test-first-development/Models/OrderItem.cs b/homework-2-test-first-development/Models/OrderItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..46e86c12d92942e6e10c33750a14ad7877fdea0a --- /dev/null +++ b/homework-2-test-first-development/Models/OrderItem.cs @@ -0,0 +1,14 @@ +namespace homework_2_test_first_development.Models; + +internal class OrderItem +{ + public string Barcode { get; set; } = string.Empty; + + public int Amount { get; set; } + + public string Name { get; set; } = string.Empty; + + public decimal TotalPrice { get; set; } + + public decimal Price { get; set; } +} diff --git a/homework-2-test-first-development/Models/Product.cs b/homework-2-test-first-development/Models/Product.cs new file mode 100644 index 0000000000000000000000000000000000000000..4f6e6538f877b9e7cf01161f4376a1510bdd39d9 --- /dev/null +++ b/homework-2-test-first-development/Models/Product.cs @@ -0,0 +1,10 @@ +namespace homework_2_test_first_development.Models; + +public class Product +{ + public string Name { get; set; } = string.Empty; + + public decimal Price { get; set; } + + public string Barcode { get; set; } = string.Empty; +} diff --git a/homework-2-test-first-development/Models/Receipt.cs b/homework-2-test-first-development/Models/Receipt.cs new file mode 100644 index 0000000000000000000000000000000000000000..6bc02526d46d185f23c93b000c7a12e97b6792ac --- /dev/null +++ b/homework-2-test-first-development/Models/Receipt.cs @@ -0,0 +1,8 @@ +namespace homework_2_test_first_development.Models; + +internal class Receipt +{ + public IEnumerable Products { get; set; } = new List(); + + public decimal TotalPrice { get; set; } +} diff --git a/homework-2-test-first-development/Program.cs b/homework-2-test-first-development/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..83fa4f4d5fd1f545f64172b044a07814db23104f --- /dev/null +++ b/homework-2-test-first-development/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/homework-2-test-first-development/Services/BarcodeProcessor.cs b/homework-2-test-first-development/Services/BarcodeProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..29b0f9c831e0ca58e6aad0b216adc4a8255749c7 --- /dev/null +++ b/homework-2-test-first-development/Services/BarcodeProcessor.cs @@ -0,0 +1,41 @@ +using homework_2_test_first_development.Models; + +namespace homework_2_test_first_development.Services; + +internal static class BarcodeProcessor +{ + public static char Seperator => '-'; + + public static IEnumerable ParseBarcodes(IEnumerable selectedBarcodes) + { + List orderItems = new List(); + + foreach (var item in selectedBarcodes) + { + string barcode = string.Empty; + int amount = 1; + + if (item.Contains(Seperator)) + { + var barcodeParts = item.Split(Seperator); + + barcode = barcodeParts[0]; + amount = int.Parse(barcodeParts[1]); + } + else + { + barcode = item; + } + + orderItems.Add(new OrderItem { Barcode = barcode, Amount = amount }); + } + + return orderItems; + } + + public static IEnumerable GroupBarcodes(IEnumerable barcodesAndAmount) + { + return barcodesAndAmount.GroupBy(o => o.Barcode) + .Select(g => new OrderItem { Barcode = g.Key, Amount = g.Sum(o => o.Amount)}); + } +} diff --git a/homework-2-test-first-development/Services/ReceiptCreator.cs b/homework-2-test-first-development/Services/ReceiptCreator.cs new file mode 100644 index 0000000000000000000000000000000000000000..39cea8b62f1cdad5399414f5fd6e9d03be52fc15 --- /dev/null +++ b/homework-2-test-first-development/Services/ReceiptCreator.cs @@ -0,0 +1,29 @@ +using homework_2_test_first_development.Exceptions; +using homework_2_test_first_development.Models; + +namespace homework_2_test_first_development.Services; + +internal static class ReceiptCreator +{ + public static Receipt CreateReceipt(IEnumerable groupedBarcodes, IEnumerable products) + { + var receipt = new Receipt { TotalPrice = 0m }; + + foreach (var item in groupedBarcodes) + { + var product = products.FirstOrDefault(p => p.Barcode == item.Barcode); + + if (product is null) + throw new ProductNotFoundException(); + + item.Name = product.Name; + item.TotalPrice = item.Amount * product.Price; + item.Price = product.Price; + + receipt.Products = receipt.Products.Append(item); + receipt.TotalPrice += item.TotalPrice; + } + + return receipt; + } +} diff --git a/homework-2-test-first-development/Services/ReceiptFormatter.cs b/homework-2-test-first-development/Services/ReceiptFormatter.cs new file mode 100644 index 0000000000000000000000000000000000000000..12a929b7811f5775fe743618e5b6b7829d72800d --- /dev/null +++ b/homework-2-test-first-development/Services/ReceiptFormatter.cs @@ -0,0 +1,18 @@ +using homework_2_test_first_development.Models; + +namespace homework_2_test_first_development.Services; + +internal static class ReceiptFormatter +{ + public static string FormatReceipt(Receipt receipt) + { + string[] header = { "Receipt", "-------" }; + + var body = receipt.Products.Select( + p => $"Name: {p.Name}, Amount: {p.Amount}, Price: {p.Price:F2}, Total: {p.TotalPrice:F2}"); + + string[] summary = { "-------", $"Total: {receipt.Products.Sum(p => p.TotalPrice):F2}" }; + + return string.Join(Environment.NewLine, header.Concat(body).Concat(summary)); + } +} diff --git a/homework-2-test-first-development/Services/ReceiptPrinter.cs b/homework-2-test-first-development/Services/ReceiptPrinter.cs new file mode 100644 index 0000000000000000000000000000000000000000..8804ca9dbf51169d4c3f473319837d4ad382d8a3 --- /dev/null +++ b/homework-2-test-first-development/Services/ReceiptPrinter.cs @@ -0,0 +1,50 @@ +using homework_2_test_first_development.Enums; +using homework_2_test_first_development.Models; +using homework_2_test_first_development.Validators; + +namespace homework_2_test_first_development.Services; + +public static class ReceiptPrinter +{ + public static string PrintReceipt(IEnumerable selectedBarcodes, IEnumerable products) + { + string receiptContent = string.Empty; + + var validationResult = BarcodeValidator.ValidateBarcodes(selectedBarcodes, products); + + if (validationResult.Error != ErrorType.Valid) + { + receiptContent = CreateErrorReceipt(validationResult); + } + else + { + receiptContent = CreateValidReceipt(selectedBarcodes, products); + } + + return receiptContent; + } + + private static string CreateErrorReceipt(ValidationResult validationResult) + { + string[] header = { "ERROR", "-------" }; + + string body = validationResult.Error switch + { + ErrorType.InvalidBarcode => "The barcode cannot be recognized", + ErrorType.ProductNotFound => $"The product cannot be found: {validationResult.Barcode}", + _ => throw new ArgumentOutOfRangeException(nameof(validationResult.Error), + $"Not expected Error type: {validationResult.Error}"), + }; + + return string.Join(Environment.NewLine, header.Append(body)); + } + + private static string CreateValidReceipt(IEnumerable selectedBarcodes, IEnumerable products) + { + var barcodesAndAmount = BarcodeProcessor.ParseBarcodes(selectedBarcodes); + var groupedBarcodes = BarcodeProcessor.GroupBarcodes(barcodesAndAmount); + var receipt = ReceiptCreator.CreateReceipt(groupedBarcodes, products); + + return ReceiptFormatter.FormatReceipt(receipt); + } +} diff --git a/homework-2-test-first-development/Validators/BarcodeValidator.cs b/homework-2-test-first-development/Validators/BarcodeValidator.cs new file mode 100644 index 0000000000000000000000000000000000000000..f166e5bd1c77214b7666696ea0910948c6812dbf --- /dev/null +++ b/homework-2-test-first-development/Validators/BarcodeValidator.cs @@ -0,0 +1,50 @@ +using homework_2_test_first_development.Enums; +using homework_2_test_first_development.Models; + +namespace homework_2_test_first_development.Validators; + +internal static class BarcodeValidator +{ + public static char Seperator => '-'; + public static int Maximum => 99; + + public static ValidationResult ValidateBarcodes(IEnumerable selectedBarcodes, IEnumerable products) + { + foreach (var item in selectedBarcodes) + { + if (string.IsNullOrEmpty(item)) + return new ValidationResult { Error = ErrorType.InvalidBarcode }; + + string barcode = string.Empty; + string amount = string.Empty; + + if (item.Contains(Seperator)) + { + var barcodeParts = item.Split(Seperator); + + barcode = barcodeParts[0]; + amount = barcodeParts[1]; + + if (string.IsNullOrEmpty(amount) || amount.Any(c => !Char.IsDigit(c))) + return new ValidationResult { Error = ErrorType.InvalidBarcode }; + + int number = int.Parse(amount); + + if (number > Maximum) + return new ValidationResult { Error = ErrorType.InvalidBarcode }; + } + else + { + barcode = item; + } + + if (string.IsNullOrEmpty(barcode) || barcode.Any(c => !(Char.IsDigit(c) || Char.IsUpper(c)))) + return new ValidationResult { Error = ErrorType.InvalidBarcode }; + + if (!products.Where(p => p.Barcode == barcode).Any()) + return new ValidationResult { Error = ErrorType.ProductNotFound, Barcode = barcode }; + } + + return new ValidationResult() { Error = ErrorType.Valid }; + } +} diff --git a/homework-2-test-first-development/Validators/ValidationResult.cs b/homework-2-test-first-development/Validators/ValidationResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..91666bfcf6d78c8850d64d6d3704616fc35968b0 --- /dev/null +++ b/homework-2-test-first-development/Validators/ValidationResult.cs @@ -0,0 +1,10 @@ +using homework_2_test_first_development.Enums; + +namespace homework_2_test_first_development.Validators; + +internal record struct ValidationResult +{ + public string? Barcode { get; init; } + + public ErrorType Error { get; init; } +} diff --git a/homework-2-test-first-development/homework-2-test-first-development.csproj b/homework-2-test-first-development/homework-2-test-first-development.csproj new file mode 100644 index 0000000000000000000000000000000000000000..1dded12fccee670ee0920e6a68a53e44e5190010 --- /dev/null +++ b/homework-2-test-first-development/homework-2-test-first-development.csproj @@ -0,0 +1,11 @@ + + + + Exe + net6.0 + homework_2_test_first_development + enable + enable + + +