diff --git a/domain-model.md b/domain-model.md new file mode 100644 index 00000000..580c3342 --- /dev/null +++ b/domain-model.md @@ -0,0 +1,68 @@ +# Domain Model + +## Simplified user stories + +- [x] Must be able to add bagel to basket +- [x] Must be able to remove bagel from basket +- [x] Must be able to check if basket is full +- [x] Must be able to change basket capacity +- [x] Must be able to check if item exists in basket +- [x] Warn when user removes non-existent item from basket +- [x] Must be able to check total cost of items in basket +- [x] Must be able to check cost of bagel before adding to basket +- [x] Must be able to choose fillings for bagel +- [x] Must be able to check cost of filling before adding to bagel order +- [x] Must be able to add coffee to basket +- [x] Must be able to check cost of coffee before adding to basket +- [x] Must be able to add promotion to product + + +## Methods + +### Product (Bagel, coffee, etc.) + +| Function Name | Parameters | Behavior | Returns | +|----------------|--------------------------|--------------------------------|--------------| +| GetCost | double cost | | | +| SetCost | double newCost | | | +| AddPromo | int amount, double price | Add a new promotion on product | void | + +### Basket + +| Function Name | Parameters | Behavior | Returns | +|--------------------------|------------------------|-------------------------------------------------|---------| +| Basket | int? capacity | Constructor, sets capacity to cart | void | +| Add | string SKU, int amount | Adds product to basket | void | +| Remove | string SKU, int amount | | void | +| SetCapacity | int newCapacity | Sets the new capacity of a basket | void | +| [private] CheckDiscounts | | | | +| [private] CheckCapacity | | Checks whether the basket can fit more products | bool | +| GetTotal | | Sets the new capacity of a basket | double | +| Order | | Submits the bagel order | void | +| [override] ToString | | Generates a string representation of cart | string | + +### BasketItem +- Product +- Amount + +### Inventory + +| Function Name | Parameters | Behavior | Returns | +|---------------|-----------------------------|-----------------------------------|---------| +| Add | Product product, int amount | | | +| Remove | Product product, int amount | | | +| GetProduct | string SKU | Get product by SKU | Product | +| GetStock | | Get stock of the specific product | int | + +### Order + +| Function Name | Parameters | Behavior | Returns | +|---------------------|-----------------|---------------------------------------------------|---------| +| Order | List | Constructor | void | +| [override] ToString | | Generate string representation of order (receipt) | string | + +### OrderLine +- Product +- Amount +- Price +- Discount diff --git a/exercise.main/Basket.cs b/exercise.main/Basket.cs new file mode 100644 index 00000000..98f2d499 --- /dev/null +++ b/exercise.main/Basket.cs @@ -0,0 +1,183 @@ +namespace exercise.main; + +public class Basket +{ + private int _capacity; + private List _items; + + private readonly IInventory _inventory; + + public Basket(IInventory inventory, int capacity = 10) + { + _capacity = capacity; + _items = new List(); + _inventory = inventory; + } + public void Add(string SKU, int quantity) + { + // Do not allow overfilled bagel basket! + if (GetNumberOfItems() + quantity > _capacity) + { + throw new Exception("Basket is full"); + } + + var item = Get(SKU); + + if (ReferenceEquals(item, null)) + { + _items.Add(new BasketItem(SKU, quantity)); + return; + } + + item.Quantity = quantity; + } + + public void Remove(string SKU, int quantity) + { + var item = Get(SKU); + // Check if nullable item is null or empty + + if (!ReferenceEquals(item, null)) + { + Get(SKU).Quantity -= quantity; + } + } + + public BasketItem? Get(string SKU) + { + return _items.FirstOrDefault(x => x.SKU == SKU); + } + + public void SetCapacity(int capacity) + { + _capacity = capacity; + } + + public int GetCapacity() + { + return _capacity; + } + + private int GetNumberOfItems() + { + var numItems = 0; + + foreach (var item in _items) + { + numItems += item.Quantity; + } + + return numItems; + } + + public double GetTotal() + { + double total = 0; + + foreach (var item in _items) + { + var price = _inventory.GetProduct(item.SKU).GetPrice(); + + total += price * item.Quantity; + } + + return total; + } + + public Order Order() + { + Order order = new Order(); + Dictionary discounts = CheckDiscounts(); + + foreach (var item in _items) + { + var product = _inventory.GetProduct(item.SKU); + order.Add(product, item.Quantity); + } + + foreach (var discount in discounts) + { + order.AddModifier(discount.Key, 0, discount.Value * -1); + } + + return order; + } + + public override string ToString() + { + throw new NotImplementedException(); + } + + private Dictionary CheckDiscounts() + { + var discounts = new Dictionary(); + var numBagels = 0; + var numCoffees = 0; + var discount = 0.0; + + foreach (var basketItem in _items) + { + if (basketItem.SKU.StartsWith("BGL")) + { + numBagels += basketItem.Quantity; + } + + if (basketItem.SKU.StartsWith("COF")) + { + numCoffees += basketItem.Quantity; + } + } + + // Apply the 12 for 3.99 offer + var twelves = numBagels / 12; + discount = twelves * (12 * 0.49 - 3.99); + numBagels %= 12; + // Discount is only applied when more than 0 + if (discount > 0) + { + discounts.Add("12 for 3.99", discount); + } + + // Apply the 6 for 2.49 offer + var sixes = numBagels / 6; + discount = sixes * (6 * 0.49 - 2.49); + numBagels %= 6; + if (discount > 0) + { + discounts.Add("6 for 2.49", discount); + } + + // Coffee deal. Not to be combined with other deals + var qualifyingMealDeals = Math.Min(numBagels, numCoffees); + discount = qualifyingMealDeals * 0.5; + if (discount > 0) + { + discounts.Add("Coffee and Bagel", discount); + } + + return discounts; + } + + private bool CheckCapacity(int numNewItems) + { + throw new NotImplementedException(); + } + + private BasketItem? Contains(string SKU) + { + return Contains(SKU, []); + } + + private BasketItem? Contains(string SKU, List modifiers) + { + modifiers.Sort(); + try + { + return _items.First(x => x.SKU == SKU && x.Modifiers.SequenceEqual(modifiers)); + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/exercise.main/BasketItem.cs b/exercise.main/BasketItem.cs new file mode 100644 index 00000000..9130442e --- /dev/null +++ b/exercise.main/BasketItem.cs @@ -0,0 +1,15 @@ +namespace exercise.main; + +public class BasketItem +{ + public string SKU { get; set; } + public int Quantity { get; set; } + public List Modifiers { get; set; } + + public BasketItem(string sku, int quantity) + { + SKU = sku; + Quantity = quantity; + Modifiers = new List(); + } +} \ No newline at end of file diff --git a/exercise.main/Inventory.cs b/exercise.main/Inventory.cs new file mode 100644 index 00000000..3aaee9d6 --- /dev/null +++ b/exercise.main/Inventory.cs @@ -0,0 +1,65 @@ +using System.Collections; + +namespace exercise.main; + +public class Inventory : IInventory, IEnumerable> +{ + private Dictionary _products; + + public Inventory() + { + _products = new Dictionary(); + } + + public IEnumerator> GetEnumerator() + { + return _products.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void AddProduct(Product product, int quantity) + { + _products.Add(product, quantity); + } + + public void RemoveProduct(Product product, int quantity) + { + throw new NotImplementedException(); + } + + public void SetStock(Product product, int quantity) + { + throw new NotImplementedException(); + } + + public int GetStock(Product product) + { + throw new NotImplementedException(); + } + + public Product GetProduct(string sku) + { + try + { + + return _products.First(p => p.Key.Sku == sku).Key; + } + catch (Exception e) + { + throw new Exception("Product not found", e); + } + } +} + +public interface IInventory +{ + void AddProduct(Product product, int quantity); + void RemoveProduct(Product product, int quantity); + void SetStock(Product product, int quantity); + int GetStock(Product product); + Product GetProduct(string sku); +} \ No newline at end of file diff --git a/exercise.main/Order.cs b/exercise.main/Order.cs new file mode 100644 index 00000000..50d6278b --- /dev/null +++ b/exercise.main/Order.cs @@ -0,0 +1,78 @@ +using System.Text; + +namespace exercise.main; + +public class Order +{ + private List _orderLines; + private DateTime _date; + + public Order() + { + _orderLines = new List(); + _date = DateTime.Now; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("------- Bob's Bagels -------\n"); + sb.AppendLine($" {_date} \n"); + + + sb.AppendLine("Product Amt Price"); + + foreach (var orderLine in _orderLines) + { + sb.AppendLine($"{ + FixedLengthString(orderLine.Product, 15)} " + + $"{FixedLengthString(orderLine.Amount.ToString(), 2)} " + + $"{FormatPrice(orderLine.Price)}"); + } + + sb.AppendLine($"\n{"Total:", 18} {FormatPrice(Total())}"); + + return sb.ToString(); + } + + public void Add(Product product, int amount) + { + _orderLines.Add(new OrderLine + { + Product = product.Name, + Amount = amount, + Price = product.GetPrice() + }); + } + + public void AddModifier(string label, int amount, double price) + { + _orderLines.Add(new OrderLine + { + Product = label, + Amount = amount, + Price = price + }); + } + + public double Total() + { + return _orderLines.Sum(ol => ol.Price * Math.Max(1, ol.Amount)); + } + + private string FixedLengthString(string value, int length) + { + return value.PadRight(length).Substring(0, length); + } + + private string FormatPrice(double price) + { + if (price < 0) + { + return $"(€{price.ToString("F2")})"; + } + + return $" €{price.ToString("F2")}"; + } +} \ No newline at end of file diff --git a/exercise.main/OrderLine.cs b/exercise.main/OrderLine.cs new file mode 100644 index 00000000..59137b49 --- /dev/null +++ b/exercise.main/OrderLine.cs @@ -0,0 +1,9 @@ +namespace exercise.main; + +public class OrderLine +{ + public string Product { get; set; } + public int Amount { get; set; } + public double Price { get; set; } + public double Discount { get; set; } +} \ No newline at end of file diff --git a/exercise.main/Product.cs b/exercise.main/Product.cs new file mode 100644 index 00000000..8c08227b --- /dev/null +++ b/exercise.main/Product.cs @@ -0,0 +1,56 @@ +namespace exercise.main; + +public class Product +{ + public string Sku { get; set; } + public string Name { get; set; } + public double Price { get; set; } + public Dictionary Promotions { get; } + public List AllowedModifications { get; set; } + public List Modifications { get; set; } + + + public Product(string sku, string name, double price) + { + Sku = sku; + Name = name; + Price = price; + Promotions = new Dictionary(); + AllowedModifications = new List(); + } + + public double GetPrice() + { + return Price; + } + + public void SetPrice(double price) + { + if (price <= 0) + { + throw new Exception("Price must be greater than 0"); + } + + Price = price; + } + + public void AddModification(ProductModification modification) + { + // Checks if the modification SKU is allowed, then add and sort + if (AllowedModifications.Any(m => m.Sku == modification.Sku)) + { + Modifications.Add(modification.Sku); + } + } + + public void AddPromotion(int quantity, double price) + { + if (quantity > 0 && price > 0) + { + Promotions.Add(quantity, price); + return; + } + + throw new Exception("Invalid promotion. Quantity and promotional price must be greater than 0"); + } +} \ No newline at end of file diff --git a/exercise.main/ProductModification.cs b/exercise.main/ProductModification.cs new file mode 100644 index 00000000..228e0f85 --- /dev/null +++ b/exercise.main/ProductModification.cs @@ -0,0 +1,8 @@ +namespace exercise.main; + +public class ProductModification : Product +{ + public ProductModification(string sku, string name, double price) : base(sku, name, price) + { + } +} \ No newline at end of file diff --git a/exercise.main/Startup.cs b/exercise.main/Startup.cs new file mode 100644 index 00000000..fa7e16a4 --- /dev/null +++ b/exercise.main/Startup.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace exercise.main; + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/exercise.main/exercise.main.csproj b/exercise.main/exercise.main.csproj index fd4bd08d..eaaf31f1 100644 --- a/exercise.main/exercise.main.csproj +++ b/exercise.main/exercise.main.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/exercise.tests/BasketTest.cs b/exercise.tests/BasketTest.cs new file mode 100644 index 00000000..1e77d12e --- /dev/null +++ b/exercise.tests/BasketTest.cs @@ -0,0 +1,45 @@ +using exercise.main; +using Microsoft.Extensions.DependencyInjection; + +namespace exercise.tests; + +[TestFixture] +public class BasketTest +{ + private Basket _basket; + private Inventory _inventory; + + [SetUp] + public void Setup() + { + _inventory = new Inventory(); + Seed.AddData(out _inventory); + _basket = new Basket(_inventory); + } + + [TestCase("BGLO", 1, 0.49)] + [TestCase("BGLP", 1, 0.39)] + [TestCase("BGLP", 3, 1.17)] + [TestCase("COFC", 1, 1.29)] + + public void TestAdd(string sku, int quantity, double total) + { + _basket.Add(sku, quantity); + Assert.AreEqual(total, _basket.GetTotal()); + } + + [Test] + public void TestRemove() + { + _basket.Add("BGLO", 1); + _basket.Remove("BGLO", 1); + Assert.AreEqual(0, _basket.GetTotal()); + } + + [Test] + public void TestSetCapacity() + { + _basket.SetCapacity(5); + Assert.AreEqual(5, _basket.GetCapacity()); + } +} \ No newline at end of file diff --git a/exercise.tests/OrderTest.cs b/exercise.tests/OrderTest.cs new file mode 100644 index 00000000..550ec289 --- /dev/null +++ b/exercise.tests/OrderTest.cs @@ -0,0 +1,47 @@ +using exercise.main; + +namespace exercise.tests; + +[TestFixture] +public class OrderTest +{ + private Inventory _inventory; + + [SetUp] + public void Setup() + { + _inventory = new Inventory(); + + Seed.AddData(out _inventory); + } + + [Test] + public void TestOrder() + { + var _basket = new Basket(_inventory); + + _basket.Add("BGLO", 3); + _basket.Add("COFB", 1); + var order = _basket.Order(); + + Console.WriteLine(order); + + Assert.IsNotEmpty(order.ToString()); + } + + [Test] + public void TestDiscounts() + { + var _basket = new Basket(_inventory); + + _basket.SetCapacity(20); + + _basket.Add("BGLO", 14); + _basket.Add("COFB", 1); + var order = _basket.Order(); + + Console.WriteLine(order); + + Assert.IsNotEmpty(order.ToString()); + } +} \ No newline at end of file diff --git a/exercise.tests/ProductModificationTest.cs b/exercise.tests/ProductModificationTest.cs new file mode 100644 index 00000000..6f73cd43 --- /dev/null +++ b/exercise.tests/ProductModificationTest.cs @@ -0,0 +1,39 @@ +using exercise.main; + +namespace exercise.tests; + +[TestFixture] +public class ProductModificationTest +{ + private static Product _product; + + [SetUp] + public void Setup() + { + _product = new Product("SKU0", "Test Product", 10.99); + _product.AllowedModifications + .Add(new ProductModification("MOD1", "Test Modification", 9.99)); + } + + [Test] + public void ProductModConstructorTest() + { + Assert.That(_product.AllowedModifications.First().Sku, Is.EqualTo("MOD1")); + Assert.That(_product.AllowedModifications.First().Name, Is.EqualTo("Test Modification")); + Assert.That(_product.AllowedModifications.First().Price, Is.EqualTo(9.99)); + } + + [Test] + public void ProductModGetPriceTest() + { + // Checks total price of product and modification + Assert.That(_product.GetPrice(), Is.EqualTo(20.99)); + } + + [Test] + public void ProductSetPriceTest() + { + _product.SetPrice(8.99); + Assert.That(_product.AllowedModifications.First().GetPrice(), Is.EqualTo(8.99)); + } +} \ No newline at end of file diff --git a/exercise.tests/ProductTest.cs b/exercise.tests/ProductTest.cs new file mode 100644 index 00000000..a689cbb9 --- /dev/null +++ b/exercise.tests/ProductTest.cs @@ -0,0 +1,56 @@ +using exercise.main; + +namespace exercise.tests; + +[TestFixture] +public class ProductTest +{ + private static Product _product; + + [SetUp] + public void Setup() + { + _product = new Product("SKU0", "Test Product", 10.99); + } + + [Test] + public void ProductConstructorTest() + { + Assert.That(_product.Sku, Is.EqualTo("SKU0")); + Assert.That(_product.Name, Is.EqualTo("Test Product")); + Assert.That(_product.Price, Is.EqualTo(10.99)); + Assert.IsEmpty(_product.Promotions); + Assert.IsEmpty(_product.AllowedModifications); + } + + [Test] + public void ProductGetPriceTest() + { + Assert.That(_product.GetPrice(), Is.EqualTo(10.99)); + } + + [TestCase(9.9)] + [TestCase(0.01)] + [TestCase(double.MaxValue)] + + public void ProductSetPriceTest(double newPrice) + { + _product.SetPrice(newPrice); + Assert.That(_product.GetPrice(), Is.EqualTo(newPrice)); + } + + [TestCase(0)] + [TestCase(-1.0)] + [TestCase(double.MinValue)] + public void ProductSetNegativePriceTest(double newPrice) + { + Assert.Throws(() => _product.SetPrice(newPrice)); + } + + [Test] + public void ProductAddPromotionTest() + { + _product.AddPromotion(2, 15.99); + Assert.That(_product.Promotions.First().Value, Is.EqualTo(15.99)); + } +} \ No newline at end of file diff --git a/exercise.tests/Seed.cs b/exercise.tests/Seed.cs new file mode 100644 index 00000000..2c5519a2 --- /dev/null +++ b/exercise.tests/Seed.cs @@ -0,0 +1,37 @@ +using exercise.main; + +namespace exercise.tests; + +public class Seed +{ + public static void AddData(out Inventory inventory) + { + inventory = new Inventory(); + inventory.AddProduct(new Product("BGLO", "Onion Bagel", 0.49), 10); + inventory.AddProduct(new Product("BGLP", "Plain Bagel", 0.39), 10); + inventory.AddProduct(new Product("BGLE", "Everything Bagel", 0.49), 10); + inventory.AddProduct(new Product("BGLS", "Sesame Bagel", 0.49), 10); + inventory.AddProduct(new Product("COFB", "Black Coffee", 0.99), 10); + inventory.AddProduct(new Product("COFW", "White Coffee", 1.19), 10); + inventory.AddProduct(new Product("COFC", "Capuccino", 1.29), 10); + inventory.AddProduct(new Product("COFL", "Latte", 1.29), 10); + + ProductModification filb = new ProductModification("FILB", "Bacon", 0.12); + ProductModification file = new ProductModification("FILE", "Egg", 0.12); + ProductModification filc = new ProductModification("FILC", "Cheese", 0.12); + ProductModification filx = new ProductModification("FILX", "Cream Cheese", 0.12); + ProductModification fils = new ProductModification("FILS", "Smoked Salmon", 0.12); + ProductModification filh = new ProductModification("FILH", "Ham", 0.12); + + List modifications = new List { filb, file, filc, filx, fils, filh }; + + foreach (KeyValuePair inventoryItem in inventory) + { + var product = inventoryItem.Key; + if (product.Sku.StartsWith("BGL")) + { + product.AllowedModifications = modifications; + } + } + } +} \ No newline at end of file diff --git a/exercise.tests/UnitTest1.cs b/exercise.tests/UnitTest1.cs index 7bdb8968..5585b540 100644 --- a/exercise.tests/UnitTest1.cs +++ b/exercise.tests/UnitTest1.cs @@ -1,10 +1,15 @@ +using exercise.main; + namespace exercise.tests; public class Tests { + protected static Inventory inventory; [SetUp] public void Setup() { + inventory = new Inventory(); + Seed.AddData(out inventory); } [Test] 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 @@ + + + +