diff --git a/domainModel.md b/domainModel.md new file mode 100644 index 00000000..d441c696 --- /dev/null +++ b/domainModel.md @@ -0,0 +1,142 @@ +# Bob's Bagels - Object-oriented Programming + + +## Domain Model of User stories + +``` +1. +As a member of the public, +So I can order a bagel before work, +I'd like to add a specific type of bagel to my basket. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|------------------------------|-------------------------------------------|-----------------| +| Basket | AddProduct(IProduct product) | Add a bagel to basket (list of bagels) | bool | + +``` +2. +As a member of the public, +So I can change my order, +I'd like to remove a bagel from my basket. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------|-----------------------------------------------|-----------------| +| Basket | RemoveProduct(string productId) | Remove a bagel from basket (list of bagels) | bool | + +``` +3. +As a member of the public, +So that I can not overfill my small bagel basket +I'd like to know when my basket is full when I try adding an item beyond my basket capacity. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------|---------------------------------------------------------------|-----------------| +| Basket | IsFull (property) | Check if basket item count exceeds basket capacity | bool | +| Basket | AddProduct(IProduct product) | Throws expection when trying to add product to full basket | exception | + +``` +4. +As a Bob's Bagels manager, +So that I can expand my business, +I’d like to change the capacity of baskets. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------|---------------------------------------------------------------|------------------| +| Basket | BasketCapacity (property) | Set basket capacity to value. Throw exception if value <= 0 | int or exception | + +``` +5. +As a member of the public +So that I can maintain my sanity +I'd like to know if I try to remove an item that doesn't exist in my basket. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------|----------------------------------------------------------------------------------|-----------------| +| Basket | RemoveProduct (string productId) | Throw exception when a product not present in basket is tried being removed | exception | + +``` +6. +As a customer, +So I know how much money I need, +I'd like to know the total cost of items in my basket. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------|----------------------------------------------|-----------------| +| Basket | BasketTotal (property) | Return the total cost of products in basket | decimal | + +``` +7. +As a customer, +So I know what the damage will be, +I'd like to know the cost of a bagel before I add it to my basket. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|---------------------------|----------------------------------------------|-----------------| +| Bagel | Price (property) | Return the price of the bagel | decimal | + +``` +8. +As a customer, +So I can shake things up a bit, +I'd like to be able to choose fillings for my bagel. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------| +| Bagel | AddFillings(IEnumerable fillings, Inventory inventory) | Adds one or multiple fillings if the fillings are in inventory bagel filling | void (Bagel.Fillings updated or exception thrown | + +``` +9. +As a customer, +So I don't over-spend, +I'd like to know the cost of each filling before I add it to my bagel order. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|---------------------------|-------------------------------------------------|-----------------| +| Filling | Price (property) | Returns the price of the filling bagel filling | decimal | + +``` +10. +As the manager, +So we don't get any weird requests, +I want customers to only be able to order things that we stock in our inventory. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|-----------------------------|---------------------------------------------------------------------------------------------------------|--------------| +| Inventory | IsInInventory | Before adding a bagel/coffee to the basket we check that the bagel/coffee variant is in the inventory | bool | +| Inventory | IsInInventory | Before adding a filling to a bagel we check that the filling variant is in the inventory | bool | + +### RECEIPT EXTENTION +``` +11. +As a customer, +So I can track what I spend money on, +I want to revieve a receipt to my order trough the console +``` +| Classes | Methods/Properties | Scenario | Outputs | +|----------------------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| +| ConsoleReceiptPrinter | Print() | Print the receipt to the terminal/console | void | +| ConsoleReceiptPrinter | PopulateOrderDictionary() | Loop through the items ordered (basket.basketItems) and store the information in the dictionary to track quantiy and subtotal | Dictionary | +| ConsoleReceiptPrinter | DateTime (property) | When the order was placed | DateTime | + +### DISCOUNT EXTENTION +``` +12. +As the manager, +When customers orders a lot I think they should recieve discount, +Special offers should be: +- Every Bagel is available for the 6 for £2.49 and 12 for £3.99 offer, but fillings still cost the extra amount per bagel. +``` +| Classes | Methods/Properties | Scenario | Outputs | +|--------------|---------------------------|-------------------------------------------------------------------------------|-----------------| +| Basket | BasketTotal (property) | Returns the total cost of products in basket, applies discounts if applicable | decimal | + +### TWILIO EXTENTION ATTEMPT +``` +13. +As a customer, +So I can track what I spend money on, +I want to revieve a receipt to my order trough twilio +``` +| Classes | Methods/Properties | Scenario | Outputs | +|----------------------------|---------------------------|---------------------------------|-----------------| +| TwilioReceiptPrinter | Print | Print the receipt to Twilio | void | \ No newline at end of file diff --git a/exercise.main/Basket.cs b/exercise.main/Basket.cs new file mode 100644 index 00000000..64f445ee --- /dev/null +++ b/exercise.main/Basket.cs @@ -0,0 +1,122 @@ +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace exercise.main +{ + public class Basket + { + private List _basketItems = new List(); + private int _basketCapacity = 5; // default capacity of the basket + private Inventory _inventory = new Inventory(); + + public Basket(Inventory inventory) + { + _inventory = inventory; + } + + public void AddProduct(IProduct product) + { + if (IsFull) + throw new InvalidOperationException("Basket is full. Cannot add more products."); + if (!_inventory.IsInInventory(product.Id)) // check if the product the customer wants to add i present in the inventory + throw new ArgumentException($"Product with SKU {product.Id} is not available in inventory."); + _basketItems.Add(product); // if it is, add it to the basket + } + + public void RemoveProduct(string productId) + { + var productToRemove = _basketItems.FirstOrDefault(p => p.Id == productId); + if (productToRemove != null) + { + _basketItems.Remove(productToRemove); + } + else + { + throw new ArgumentException("The item with this Id is not present in your basket. Could not be removed"); + } + } + + public List basketItems { get { return _basketItems; } } + + public bool IsFull { get { return _basketItems.Count >= _basketCapacity; } } + + public int BasketCapacity + { + get { return _basketCapacity; } + set + { + if (value > 0) + { + _basketCapacity = value; + } + else + { + throw new ArgumentException("Basket capacity must be greater than zero."); + } + } + + } + + /// + /// Groups all bagels by SKU. + /// For each bagel group, apply the discount for every 6 bagels with same SKU, and adds the base bagel price for any leftovers. + /// Add the price of all other products as normal to BasketTotal + /// + public decimal BasketTotal + { + get + { + decimal total = 0m; + total += GetBagelDiscountTotal(); + total += GetNonBagelTotal(); + return total; + } + } + public decimal GetBagelDiscountTotal() + { + + decimal total = 0m; + + // Group bagels by SKU + var bagelGroups = _basketItems + .Where(p => p.Name is "Bagel") + .Cast() // Cast to Bagel to access specific properties + .GroupBy(b => b.Id); + + foreach (var group in bagelGroups) // loop through each group of bagels + { + int count = group.Count(); // count how many bagels of the same SKU are in the group + decimal basePrice = Bagel.GetBasePrice(group.Key); // get the base price of the bagel + + // 12-for-£3.99 + int setsOf12 = count / 12; + int remainderAfter12 = count % 12; + + // 6-for-£2.49 + int setsOf6 = remainderAfter12 / 6; + int remainder = remainderAfter12 % 6; + + total += setsOf12 * 3.99m; // add the price for every 12 bagels + total += setsOf6 * 2.49m; // add the price for every 6 bagels + total += remainder * basePrice; // add the price for any leftover bagels that do not fit into a 6 or 12 set + + // Sums the price of all fillings for all bagels in each group and adds it to the total. + total += group.SelectMany(b => b.Fillings).Sum(f => f.Price); + } + return total; + } + + // Add all non-bagel products (coffee, fillings) + public decimal GetNonBagelTotal() + { + return _basketItems + .Where(p => p.Name is not "Bagel") + .Sum(p => p.Price); + } + } +} \ No newline at end of file diff --git a/exercise.main/ConsoleReceiptPrinter.cs b/exercise.main/ConsoleReceiptPrinter.cs new file mode 100644 index 00000000..69b84dd8 --- /dev/null +++ b/exercise.main/ConsoleReceiptPrinter.cs @@ -0,0 +1,80 @@ +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main +{ + public class ConsoleReceiptPrinter : IReceiptPrinter + { + private string _header = " ~~~ Bob's Bagels ~~~"; + private string _thankuMessage1 = " Thank you"; + private string _thankuMessage2 = " for your order!"; + private string _separationSymbol = "----------------------------"; + private Basket _basket; + + // Dictionary to store the ordered items with associated details + private Dictionary orderDict = + new Dictionary(); + + public ConsoleReceiptPrinter(Basket basket) + { + _basket = basket; + DateTime = DateTime.Now; + } + + // method to loop through the items ordered (basket.basketItems) and store the information in the dictionary + private void PopulateOrderDictionary() + { + orderDict.Clear(); + foreach (IProduct product in _basket.basketItems) + { + string key = product.Id; + if (orderDict.ContainsKey(key)) + { + // If the item already exists in the dictionary, increment the quantity and update subtotal + var existingItem = orderDict[key]; + existingItem.Quantity++; + existingItem.Subtotal += product.Price; + orderDict[key] = existingItem; + } + else + { + // If it's a new item, add it to the dictionary, base count 1 + orderDict[key] = (product.Name, product.Variant, 1, product.Price); + } + } + } + + public DateTime DateTime { get; set; } + + public void Print() + { + PopulateOrderDictionary(); + + Console.WriteLine(_header); + Console.WriteLine($"\n {DateTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + Console.WriteLine($"\n{_separationSymbol}\n"); + + foreach (var item in orderDict) + { + var productDetails = item.Value; + string itemName = $"{productDetails.Variant} {productDetails.Name}".Trim(); + string qty = productDetails.Quantity.ToString(); + string subtotal = productDetails.Subtotal.ToString("£0.00", CultureInfo.InvariantCulture); + + Console.WriteLine("{0,-16} {1,3} {2,7}", itemName, qty, subtotal); + } + + Console.WriteLine($"\n{_separationSymbol}\n"); + string totalLabel = "Total"; + string totalValue = _basket.BasketTotal.ToString("£0.00", CultureInfo.InvariantCulture).PadLeft(28 - totalLabel.Length); + Console.WriteLine(totalLabel + totalValue); + Console.WriteLine($"\n{_thankuMessage1}"); + Console.WriteLine($"{_thankuMessage2}"); + } + } +} \ No newline at end of file diff --git a/exercise.main/IReceiptPrinter.cs b/exercise.main/IReceiptPrinter.cs new file mode 100644 index 00000000..8263443d --- /dev/null +++ b/exercise.main/IReceiptPrinter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main +{ + public interface IReceiptPrinter + { + void Print(); + } +} \ No newline at end of file diff --git a/exercise.main/Inventory.cs b/exercise.main/Inventory.cs new file mode 100644 index 00000000..504a5b84 --- /dev/null +++ b/exercise.main/Inventory.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main +{ + /// + /// Ensures that customers are only able to order things that are stocked in inventory. + /// + public class Inventory + { + private HashSet _inventory = new HashSet() + { + "BGLO", "BGLP", "BGLE", "BGLS", // bagel variant SKUs + "FILB", "FILE", "FILC", "FILX", "FILS", "FILH", // filling variant SKUs + "COFB", "COFW", "COFC", "COFL" // coffee variant SKUs + }; + + public bool IsInInventory(string productId) + { + return _inventory.Contains(productId); + } + } +} diff --git a/exercise.main/Products/Bagel.cs b/exercise.main/Products/Bagel.cs new file mode 100644 index 00000000..74442725 --- /dev/null +++ b/exercise.main/Products/Bagel.cs @@ -0,0 +1,80 @@ +using System; + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Bagel : IProduct + { + + private static readonly Dictionary VariantPrices = new Dictionary() + { + {"BGLO", 0.49m}, // Onion + {"BGLP", 0.39m}, // Plain + {"BGLE", 0.49m}, // Everything + {"BGLS", 0.49m} // Sesame + }; + + private static readonly Dictionary VariantNames = new Dictionary() + { + {"BGLO", "Onion"}, + {"BGLP", "Plain"}, + {"BGLE", "Everything"}, + {"BGLS", "Sesame"} + }; + + private string _id; + private List _fillings = new List(); + + public Bagel(string id) + { + _id = id; + } + + public string Name => "Bagel"; + + public decimal Price + { + get + { + if (!VariantPrices.ContainsKey(_id)) + throw new ArgumentException($"Invalid bagel id: {_id}"); + decimal price = VariantPrices[_id]; // get the spesific bagel´s price + price += _fillings.Sum(f => f.Price); // returns 0 if the bagel have no fillings + return price; + } + } + + public string Variant + { + get + { + if (!VariantNames.ContainsKey(_id)) + throw new ArgumentException($"Invalid bagel id: {_id}"); + return VariantNames[_id]; + } + } + + public string Id { get { return _id; } } + + public IReadOnlyList Fillings => _fillings.AsReadOnly(); + public void AddFillings(IEnumerable fillings, Inventory inventory) + { + foreach (var filling in fillings) + { + if (!inventory.IsInInventory(filling.Id)) + throw new ArgumentException($"Filling {filling.Id} is not in inventory."); + _fillings.Add(filling); + } + } + + // Used when calculating BasketTotal and adding discount, since the discount applies to the bagel itself, not to any fillings. + public static decimal GetBasePrice(string id) + { + return VariantPrices.ContainsKey(id) ? VariantPrices[id] : 0m; + } + } +} \ No newline at end of file diff --git a/exercise.main/Products/Coffee.cs b/exercise.main/Products/Coffee.cs new file mode 100644 index 00000000..9f6f5a4f --- /dev/null +++ b/exercise.main/Products/Coffee.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Coffee : IProduct + { + + private static readonly Dictionary VariantPrices = new Dictionary() + { + {"COFB", 0.99m}, // Black coffee + {"COFW", 1.19m}, // White coffee + {"COFC", 1.29m}, // Capuccino + {"COFL", 1.29m} // Latte + }; + + private static readonly Dictionary VariantNames = new Dictionary() + { + {"COFB", "Black"}, + {"COFW", "White"}, + {"COFC", "Capuccino"}, + {"COFL", "Latte"} + }; + + private string _id; + + public Coffee(string id) + { + _id = id; + } + public string Name => "Coffee"; + + public decimal Price + { + get + { + if (!VariantPrices.ContainsKey(_id)) + throw new ArgumentException($"Invalid coffee id: {_id}"); + decimal price = VariantPrices[_id]; // get the spesific coffee´s price + return price; + } + } + + public string Variant + { + get + { + if (!VariantNames.ContainsKey(_id)) + throw new ArgumentException($"Invalid coffee id: {_id}"); + return VariantNames[_id]; + } + } + + public string Id { get { return _id; } } + } +} \ No newline at end of file diff --git a/exercise.main/Products/Filling.cs b/exercise.main/Products/Filling.cs new file mode 100644 index 00000000..6d699837 --- /dev/null +++ b/exercise.main/Products/Filling.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Filling : IProduct + { + private static readonly Dictionary VariantNames = new Dictionary() + { + {"FILB", "Bacon"}, + {"FILE", "Egg"}, + {"FILC", "Cheese"}, + {"FILX", "Creamy Cheese"}, + {"FILS", "Smoked Salmon"}, + {"FILH", "Ham"} + }; + + private string _id; + + public Filling(string id) + { + _id = id; + } + public string Name => "Filling"; + + public decimal Price => 0.12m; // price is the same for every filling variant + + public string Variant + { + get + { + if (!VariantNames.ContainsKey(_id)) + throw new ArgumentException($"Invalid filling id: {_id}"); + return VariantNames[_id]; + } + } + + public string Id { get { return _id; } } + } +} diff --git a/exercise.main/Products/IProduct.cs b/exercise.main/Products/IProduct.cs new file mode 100644 index 00000000..7091755f --- /dev/null +++ b/exercise.main/Products/IProduct.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public interface IProduct + { + string Name { get; } + decimal Price { get; } + string Variant { get; } + string Id { get; } + } +} diff --git a/exercise.main/Program.cs b/exercise.main/Program.cs index 3751555c..d3a37c8e 100644 --- a/exercise.main/Program.cs +++ b/exercise.main/Program.cs @@ -1,2 +1,54 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); + +using exercise.main; +using exercise.main.Products; +using System.Reflection.Metadata; + +Inventory inventory = new Inventory(); +Basket basket = new Basket(inventory); +basket.BasketCapacity = 50; + +basket.AddProduct(new Bagel("BGLO")); +basket.AddProduct(new Bagel("BGLO")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLP")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Bagel("BGLE")); +basket.AddProduct(new Coffee("COFB")); +basket.AddProduct(new Coffee("COFB")); +basket.AddProduct(new Coffee("COFB")); + +// Can change which receipt printer to use, either console or twilio +IReceiptPrinter consoleReceipt = new ConsoleReceiptPrinter(basket); +consoleReceipt.Print(); + +// Should print something like this +// ~~~Bob's Bagels ~~~ + +// 2021 - 03 - 16 21:38:44 + +//---------------------------- + +//Onion Bagel 2 £0.98 +//Plain Bagel 12 £3.99 +//Everything Bagel 6 £2.49 +//Black Coffee 3 £2.97 + +//---------------------------- +//Total £10.43 + +// Thank you +// for your order! \ No newline at end of file diff --git a/exercise.main/TwilioReceiptPrinter.cs b/exercise.main/TwilioReceiptPrinter.cs new file mode 100644 index 00000000..105197d0 --- /dev/null +++ b/exercise.main/TwilioReceiptPrinter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Twilio; +using Twilio.Rest.Api.V2010.Account; +using System.Threading.Tasks; + +namespace exercise.main +{ + /// + /// NB! I have not been able to test this class, as I do not have a Twilio account. + /// + public class TwilioReceiptPrinter : IReceiptPrinter + { + private string _accountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID"); + private string _authToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN"); + public TwilioReceiptPrinter() { + TwilioClient.Init(_accountSid, _authToken); + } + + public async void Print() + { + var message = await MessageResource.CreateAsync( + body: "Join Earth's mightiest heroes. Like Kevin Bacon.", + from: new Twilio.Types.PhoneNumber("+15017122661"), + to: new Twilio.Types.PhoneNumber("+15558675310")); + } + } +} \ No newline at end of file diff --git a/exercise.main/exercise.main.csproj b/exercise.main/exercise.main.csproj index fd4bd08d..2e5676af 100644 --- a/exercise.main/exercise.main.csproj +++ b/exercise.main/exercise.main.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/exercise.sln b/exercise.sln index 0efb5453..092c8cd6 100644 --- a/exercise.sln +++ b/exercise.sln @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exercise.tests", "exercise. EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{825CCFE7-4F2E-4770-8393-FEB732F66EE4}" ProjectSection(SolutionItems) = preProject + domainModel.md = domainModel.md extension1.md = extension1.md extension2.md = extension2.md extension3.md = extension3.md @@ -34,4 +35,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AA407FFD-6646-46D3-A264-678F5333229D} + EndGlobalSection EndGlobal diff --git a/exercise.tests/ConsoleReceiptPrinterTests.cs b/exercise.tests/ConsoleReceiptPrinterTests.cs new file mode 100644 index 00000000..c928d513 --- /dev/null +++ b/exercise.tests/ConsoleReceiptPrinterTests.cs @@ -0,0 +1,36 @@ +using exercise.main; +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.tests +{ + public class ConsoleReceiptPrinterTests + { + [Test] // user story 11, printing receipt to console works + public void PrintReceipt() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + ConsoleReceiptPrinter receipt = new ConsoleReceiptPrinter(basket); + + // redirect console output + var output = new StringWriter(); + Console.SetOut(output); + + // act + receipt.Print(); + + // assert + string printed = output.ToString(); + Assert.IsNotEmpty(printed); // Check that something actualy was printed + Assert.That(printed, Does.Contain("Bob's Bagels")); + } + } +} \ No newline at end of file diff --git a/exercise.tests/CoreTests.cs b/exercise.tests/CoreTests.cs new file mode 100644 index 00000000..bda3ef34 --- /dev/null +++ b/exercise.tests/CoreTests.cs @@ -0,0 +1,249 @@ +using exercise.main; +using exercise.main.Products; + +namespace exercise.tests; + +public class CoreTests +{ + + [Test] // user story 1 + public void AddBagelToBasket() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + IProduct onionBagel = new Bagel("BGLO"); // adds a specific Bagel variant (onion bagel) + + // act + basket.AddProduct(onionBagel); + + // assert + Assert.That(basket.basketItems.Count, Is.EqualTo(1)); + Assert.That(basket.basketItems[0].Id, Is.EqualTo("BGLO")); + Assert.That(basket.basketItems[0].Variant, Is.EqualTo("Onion")); + } + + [Test] // user story 2 + public void RemoveBagelFromBasket() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + IProduct sesameBagel = new Bagel("BGLS"); + basket.AddProduct(sesameBagel); + + // assert that the sesame bagel is in the basket + Assert.That(basket.basketItems.Count, Is.EqualTo(1)); + Assert.That(basket.basketItems, Does.Contain(sesameBagel)); + + // act + basket.RemoveProduct("BGLS"); // removes the Bagel with ID "BGLO" + + // assert + Assert.That(basket.basketItems.Count, Is.EqualTo(0)); + Assert.That(basket.basketItems, Does.Not.Contain(sesameBagel)); + } + + [Test] // user story 3 + public void BasketIsFull() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLS")); + basket.AddProduct(new Bagel("BGLS")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLS")); + + // act + bool isFull = basket.IsFull; + + // assert + Assert.That(isFull, Is.True); + Assert.Throws(() => basket.AddProduct(new Bagel("BGLE"))); // trying to add another bagel should throw an exception + } + + [Test] // user story 3 + public void BasketIsNotFull() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLS")); + basket.AddProduct(new Bagel("BGLS")); + basket.AddProduct(new Bagel("BGLP")); + + // act + bool isFull = basket.IsFull; + + // assert + Assert.That(isFull, Is.False); + Assert.DoesNotThrow(() => basket.AddProduct(new Bagel("BGLE"))); // adding another bagel should not throw an exception + } + + [Test] // user story 4 + public void BasketCapacityCanBeSet() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + + // act + basket.BasketCapacity = 10; + + // assert + Assert.That(basket.BasketCapacity, Is.EqualTo(10)); + } + + [Test] // user story 4 + public void BasketCapacityCannotBeSetToZeroOrNegative() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + + // act & assert + Assert.Throws(() => basket.BasketCapacity = 0); + Assert.Throws(() => basket.BasketCapacity = -5); + } + + [Test] // user story 5 + public void RemoveNonExistentProduct() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.AddProduct(new Bagel("BGLS")); // adds a sesame bagel to basket + + // act & assert + Assert.Throws(() => basket.RemoveProduct("BGLO")); // removing a non-present onion bagel is not allowed + } + + [Test] // user story 6 + public void BasketTotal1() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLS")); + + // act + decimal basketTotal = basket.BasketTotal; + + // assert + Assert.That(basketTotal != 0); + Assert.That(basketTotal, Is.EqualTo(1.37m)); // 0.49 + 0.39 + 0.49 = 1.37 + } + + [Test] // user story 6: make sure that it also adds the price for the bagel fillings to BasketTotal + public void BasketTotal2() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + + Filling cheese = new Filling("FILC"); + Filling bacon = new Filling("FILB"); + + Bagel onionBagel = new Bagel("BGLO"); + onionBagel.AddFillings(new[] { cheese, bacon }, inventory); // add fillings to the onion bagel + basket.AddProduct(onionBagel); // adds the onion bagel with fillings to the basket + + Bagel sesameBagel = new Bagel("BGLS"); + sesameBagel.AddFillings(new[] { cheese }, inventory); // add filling to the sesame bagel + basket.AddProduct(sesameBagel); // adds the sesame bagel with filling to the basket + + // act + decimal basketTotal = basket.BasketTotal; + + // assert + Assert.That(basketTotal != 0); + Assert.That(basketTotal, Is.EqualTo(1.34m)); // 0.49 (onion bagel) + 0.12 (bacon filling) + 0.12 (cheese filling) + 0.49 (sesame bagel) + 0.12 (cheese filling) = 1.34 + } + + [Test] // user story 7 + public void BagelPrice() + { + // arrange + Bagel onionBagel = new Bagel("BGLO"); + Bagel plainBagel = new Bagel("BGLP"); + Bagel everythingBagel = new Bagel("BGLE"); + Bagel sesameBagel = new Bagel("BGLS"); + + // act and assert + Assert.That(onionBagel.Price, Is.EqualTo(0.49m)); + Assert.That(plainBagel.Price, Is.EqualTo(0.39m)); + Assert.That(everythingBagel.Price, Is.EqualTo(0.49m)); + Assert.That(sesameBagel.Price, Is.EqualTo(0.49m)); + } + + [Test] // user story 8 + public void ChooseBagelFilling() + { + // arrange + Inventory inventory = new Inventory(); + Filling baconFilling = new Filling("FILB"); + Filling cheeseFilling = new Filling("FILC"); + Bagel onionBagel = new Bagel("BGLO"); + + // act + onionBagel.AddFillings(new[] { baconFilling, cheeseFilling }, inventory); + + // assert + Assert.That(onionBagel.Fillings.Count, Is.EqualTo(2)); + Assert.That(onionBagel.Fillings.Any(f => f.Id == "FILB")); + } + + [Test] // user story 9 + public void FillingPrice() + { + // arrange + Filling baconFilling = new Filling("FILB"); + Filling eggFilling = new Filling("FILE"); + Filling cheeseFilling = new Filling("FILC"); + Filling creamyCheeseFilling = new Filling("FILX"); + Filling hamFilling = new Filling("FILH"); + + // act and assert + Assert.That(baconFilling.Price, Is.EqualTo(0.12m)); + Assert.That(eggFilling.Price, Is.EqualTo(0.12m)); + Assert.That(cheeseFilling.Price, Is.EqualTo(0.12m)); + Assert.That(creamyCheeseFilling.Price, Is.EqualTo(0.12m)); + Assert.That(hamFilling.Price, Is.EqualTo(0.12m)); + } + + [Test] // user story 10; check that a user cannot add a product that is not in inventory + public void AddNonExistingProductToBasket() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + Bagel onionBagel = new Bagel("BGLO"); + Bagel fishBagel = new Bagel("BGLF"); + + // act + basket.AddProduct(onionBagel); // add the onion bagel to the basket + // assert that onion bagel was added + Assert.That(basket.basketItems.Count, Is.EqualTo(1)); + + // act & assert + Assert.Throws(() => basket.AddProduct(fishBagel)); // try do add the fishBagel to the basket (fish bagel is not in the inventory) + } + + [Test] // user story 10; check that a user cannot add a filling that is not in inventory to a bagel + public void AddNonExistingFillingToBagel() + { + // arrange + Inventory inventory = new Inventory(); + Bagel onionBagel = new Bagel("BGLO"); + Filling cheese = new Filling("FILC"); + Filling paprika = new Filling("FILP"); + + // act & assert + Assert.Throws(() => onionBagel.AddFillings(new[] {paprika, cheese}, inventory)); // try do add paprika filling to the onion bagel (paprika filling is not in the inventory) + } +} \ No newline at end of file diff --git a/exercise.tests/DiscountExtentionTests.cs b/exercise.tests/DiscountExtentionTests.cs new file mode 100644 index 00000000..972f7759 --- /dev/null +++ b/exercise.tests/DiscountExtentionTests.cs @@ -0,0 +1,141 @@ +using exercise.main; +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.tests +{ + public class DiscountExtentionTests + { + [Test] // user story 12, Every Bagel should be available for the 6 for £2.49. Must be 6 with same SKU + public void BuyingSixBagels() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.BasketCapacity = 10; // must adjust basket capacity + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + + // act & assert + Assert.That(basket.BasketTotal, Is.EqualTo(2.49)); // True if discount was added + } + + [Test] // user story 12, Buying 5 with same SKU and one with another SKU should not apply discount + public void BuyingFiveBagels() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.BasketCapacity = 10; // must adjust basket capacity + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLP")); + + // act & assert + Assert.That(basket.BasketTotal, Is.EqualTo(2.84)); // 5 * 0.49 + 0.39 = 2.94, no discount applied + } + + [Test] // user story 12, Every Bagel should be available for the 12 for £3.99. Must be 12 with same SKU + public void BuyingTwelveBagels() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.BasketCapacity = 12; // must adjust basket capacity + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + + // act & assert + Assert.That(basket.BasketTotal, Is.EqualTo(3.99)); // True if discount was added + } + + [Test] // user story 12, Full order discount test + public void DiscountedOrder1() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.BasketCapacity = 30; // must adjust basket capacity + // add 2 onion bagels: price is 0.98 for 2 + basket.AddProduct(new Bagel("BGLO")); + basket.AddProduct(new Bagel("BGLO")); + // add 12 plain bagels: price is 3.99 for 12 + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + // add 6 everything bagels: price is 2.49 for 6 + basket.AddProduct(new Bagel("BGLE")); + basket.AddProduct(new Bagel("BGLE")); + basket.AddProduct(new Bagel("BGLE")); + basket.AddProduct(new Bagel("BGLE")); + basket.AddProduct(new Bagel("BGLE")); + basket.AddProduct(new Bagel("BGLE")); + // add 3 black coffees: price is 2.97 for 3 + basket.AddProduct(new Coffee("COFB")); + basket.AddProduct(new Coffee("COFB")); + basket.AddProduct(new Coffee("COFB")); + + // act & assert + Assert.That(basket.BasketTotal, Is.EqualTo(10.43)); // True if discount was added + } + + [Test] // user story 12, Full order discount test + public void DiscountedOrder2() + { + // arrange + Inventory inventory = new Inventory(); + Basket basket = new Basket(inventory); + basket.BasketCapacity = 30; // must adjust basket capacity + // add 16 plain bagels: price is £3.99 for 12 and £0.39 for each additional bagel + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + basket.AddProduct(new Bagel("BGLP")); + + // act & assert + Assert.That(basket.BasketTotal, Is.EqualTo(5.55)); // True if discount was added + } + } +} \ No newline at end of file diff --git a/exercise.tests/TwilioReceiptPrinterTests.cs b/exercise.tests/TwilioReceiptPrinterTests.cs new file mode 100644 index 00000000..daef0355 --- /dev/null +++ b/exercise.tests/TwilioReceiptPrinterTests.cs @@ -0,0 +1,20 @@ +using exercise.main; +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.tests +{ + public class TwilioReceiptPrinterTests + { + [Test] // user story 13, printing receiptto twilio works + public void PrintReceipt() + { + // I have not implemented a test for this since it requires a live Twilio account and phone number and I don't have one. + Assert.Pass("Twilio receipt printing requires a live account and phone number, test not implemented."); + } + } +} diff --git a/exercise.tests/UnitTest1.cs b/exercise.tests/UnitTest1.cs deleted file mode 100644 index 7bdb8968..00000000 --- a/exercise.tests/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace exercise.tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/exercise.tests/exercise.tests.csproj b/exercise.tests/exercise.tests.csproj index 9fed8e17..a3a97d4f 100644 --- a/exercise.tests/exercise.tests.csproj +++ b/exercise.tests/exercise.tests.csproj @@ -17,4 +17,8 @@ + + + +