From 18bd822c181314134e62a27520ae35730f3201db Mon Sep 17 00:00:00 2001 From: Mona Eikli Andresen Date: Mon, 11 Aug 2025 16:02:52 +0200 Subject: [PATCH 1/3] Mona Eikli --- exercise.main/Basket.cs | 21 +++ exercise.main/Inventory/Catalog.cs | 67 +++++++ exercise.main/Inventory/CatalogItem.cs | 31 ++++ exercise.main/Inventory/ICatalog.cs | 19 ++ exercise.main/Products/Bagel.cs | 15 ++ exercise.main/Products/Coffee.cs | 15 ++ exercise.main/Products/Filling.cs | 15 ++ exercise.main/Products/IDiscountable.cs | 13 ++ exercise.main/Products/IProduct.cs | 19 ++ exercise.main/Products/Product.cs | 31 ++++ exercise.main/Products/ProductType.cs | 16 ++ exercise.main/Program.cs | 231 +++++++++++++++++++++++- exercise.main/domain_model.md | 30 +++ exercise.tests/UnitTest1.cs | 72 +++++++- exercise.tests/exercise.tests.csproj | 4 + 15 files changed, 592 insertions(+), 7 deletions(-) create mode 100644 exercise.main/Basket.cs create mode 100644 exercise.main/Inventory/Catalog.cs create mode 100644 exercise.main/Inventory/CatalogItem.cs create mode 100644 exercise.main/Inventory/ICatalog.cs create mode 100644 exercise.main/Products/Bagel.cs create mode 100644 exercise.main/Products/Coffee.cs create mode 100644 exercise.main/Products/Filling.cs create mode 100644 exercise.main/Products/IDiscountable.cs create mode 100644 exercise.main/Products/IProduct.cs create mode 100644 exercise.main/Products/Product.cs create mode 100644 exercise.main/Products/ProductType.cs create mode 100644 exercise.main/domain_model.md diff --git a/exercise.main/Basket.cs b/exercise.main/Basket.cs new file mode 100644 index 00000000..2a318dab --- /dev/null +++ b/exercise.main/Basket.cs @@ -0,0 +1,21 @@ +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace exercise.main +{ + public class Basket + { + private readonly List _items = new(); + + public void AddProduct(IProduct product) => _items.Add(product); + + public List Products => _items; + + public decimal TotalPrice => _items.Sum(p => p.Price); + } +} diff --git a/exercise.main/Inventory/Catalog.cs b/exercise.main/Inventory/Catalog.cs new file mode 100644 index 00000000..72a21c88 --- /dev/null +++ b/exercise.main/Inventory/Catalog.cs @@ -0,0 +1,67 @@ +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Inventory +{ + public class Catalog : ICatalog + { + private static int _nextId = 1; + private readonly Dictionary _items; + + public Catalog() + { + _items = new(StringComparer.OrdinalIgnoreCase) + { + ["BGLO"] = new("BGLO", 0.49m, "Bagel", "Onion", ProductType.Bagel, true), + ["BGLP"] = new("BGLP", 0.39m, "Bagel", "Plain", ProductType.Bagel, true), + ["BGLE"] = new("BGLE", 0.49m, "Bagel", "Everything", ProductType.Bagel, true), + ["BGLS"] = new("BGLS", 0.49m, "Bagel", "Sesame", ProductType.Bagel, true), + + ["COFB"] = new("COFB", 0.99m, "Coffee", "Black", ProductType.Coffee, true), + ["COFW"] = new("COFW", 1.19m, "Coffee", "White", ProductType.Coffee, true), + ["COFC"] = new("COFC", 1.29m, "Coffee", "Cappuccino", ProductType.Coffee, false), + ["COFL"] = new("COFL", 1.29m, "Coffee", "Latte", ProductType.Coffee, false), + + ["FILB"] = new("FILB", 0.12m, "Filling", "Bacon", ProductType.Filling, true), + ["FILE"] = new("FILE", 0.12m, "Filling", "Egg", ProductType.Filling, true), + ["FILC"] = new("FILC", 0.12m, "Filling", "Cheese", ProductType.Filling, true), + ["FILX"] = new("FILX", 0.12m, "Filling", "Cream Cheese", ProductType.Filling, true), + ["FILS"] = new("FILS", 0.12m, "Filling", "Smoked Salmon", ProductType.Filling, true), + ["FILH"] = new("FILH", 0.12m, "Filling", "Ham", ProductType.Filling, true), + }; + } + + public bool Has(string sku) => _items.ContainsKey(sku); + + public CatalogItem Get(string sku) => + _items.TryGetValue(sku, out var item) ? item + : throw new KeyNotFoundException($"SKU '{sku}' not found."); + + public IEnumerable GetByType(ProductType type, bool onlyInStock = true) => + _items.Values.Where(i => i.Type == type && (!onlyInStock || i.InStock)); + + public IEnumerable GetSoldOutByType(ProductType type) => + _items.Values.Where(i => i.Type == type && !i.InStock); + + public IProduct CreateProduct(string sku) + { + var i = Get(sku); + if (!i.InStock) throw new InvalidOperationException($"{i.Name} - {i.Variant} is out of stock."); + var id = _nextId++; + + return i.Type switch + { + ProductType.Bagel => new Bagel(id, i.Sku, i.Variant, i.Price), + ProductType.Coffee => new Coffee(id, i.Sku, i.Variant, i.Price), + ProductType.Filling => new Filling(id, i.Sku, i.Variant, i.Price), + _ => throw new NotSupportedException($"Unsupported type for SKU '{i.Sku}'.") + }; + } + + public decimal GetPrice(string sku) => Get(sku).Price; + } +} diff --git a/exercise.main/Inventory/CatalogItem.cs b/exercise.main/Inventory/CatalogItem.cs new file mode 100644 index 00000000..2b4b9fe4 --- /dev/null +++ b/exercise.main/Inventory/CatalogItem.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using exercise.main.Products; + +namespace exercise.main.Inventory +{ + public class CatalogItem + { + public string Sku { get; } + public decimal Price { get; } + public string Name { get; } + public string Variant { get; } + public ProductType Type { get; } + public bool InStock { get; set; } + + public CatalogItem(string sku, decimal price, string name, string variant, ProductType type, bool inStock = true) + { + Sku = sku; + Price = price; + Name = name; + Variant = variant; + Type = type; + InStock = inStock; + } + } +} + diff --git a/exercise.main/Inventory/ICatalog.cs b/exercise.main/Inventory/ICatalog.cs new file mode 100644 index 00000000..730977a6 --- /dev/null +++ b/exercise.main/Inventory/ICatalog.cs @@ -0,0 +1,19 @@ +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Inventory +{ + public interface ICatalog + { + bool Has(string sku); + CatalogItem Get(string sku); + IEnumerable GetByType(ProductType type, bool onlyInStock = true); + IEnumerable GetSoldOutByType(ProductType type); + IProduct CreateProduct(string sku); // makes a concrete Bagel/Coffee/Filling + decimal GetPrice(string sku); + } +} diff --git a/exercise.main/Products/Bagel.cs b/exercise.main/Products/Bagel.cs new file mode 100644 index 00000000..3932d4da --- /dev/null +++ b/exercise.main/Products/Bagel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Bagel : Product + { + public Bagel(int id, string sku, string variant, decimal price) + : base(id, sku, "Bagel", variant, price, ProductType.Bagel) { } + } +} + diff --git a/exercise.main/Products/Coffee.cs b/exercise.main/Products/Coffee.cs new file mode 100644 index 00000000..9551062e --- /dev/null +++ b/exercise.main/Products/Coffee.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Coffee : Product + { + public Coffee(int id, string sku, string variant, decimal price) + : base(id, sku, "Coffee", variant, price, ProductType.Coffee) { } + } +} + diff --git a/exercise.main/Products/Filling.cs b/exercise.main/Products/Filling.cs new file mode 100644 index 00000000..99367b54 --- /dev/null +++ b/exercise.main/Products/Filling.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public class Filling : Product + { + public Filling(int id, string sku, string variant, decimal price) + : base(id, sku, "Filling", variant, price, ProductType.Filling) { } + } +} + diff --git a/exercise.main/Products/IDiscountable.cs b/exercise.main/Products/IDiscountable.cs new file mode 100644 index 00000000..d7aaf9df --- /dev/null +++ b/exercise.main/Products/IDiscountable.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public interface IDiscountable + { + + } +} diff --git a/exercise.main/Products/IProduct.cs b/exercise.main/Products/IProduct.cs new file mode 100644 index 00000000..68c656a2 --- /dev/null +++ b/exercise.main/Products/IProduct.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public interface IProduct + { + int Id { get; } + string Sku { get; } + string Name { get; } + string Variant { get; } + decimal Price { get; } + ProductType Type { get; } + } +} + diff --git a/exercise.main/Products/Product.cs b/exercise.main/Products/Product.cs new file mode 100644 index 00000000..fdb725c6 --- /dev/null +++ b/exercise.main/Products/Product.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace exercise.main.Products +{ + public abstract class Product : IProduct + { + public int Id { get; } + public string Sku { get; } + public string Name { get; } + public string Variant { get; } + public decimal Price { get; } + public ProductType Type { get; } + + protected Product(int id, string sku, string name, string variant, decimal price, ProductType type) + { + Id = id; + Sku = sku; + Name = name; + Variant = variant; + Price = price; + Type = type; + } + + public override string ToString() => $"{Name} - {Variant} ({Sku}) ${Price:0.00}"; + } +} + diff --git a/exercise.main/Products/ProductType.cs b/exercise.main/Products/ProductType.cs new file mode 100644 index 00000000..89b4d58d --- /dev/null +++ b/exercise.main/Products/ProductType.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 enum ProductType + { + Bagel = 1, + Coffee = 2, + Filling = 3 + } +} + diff --git a/exercise.main/Program.cs b/exercise.main/Program.cs index 3751555c..86222488 100644 --- a/exercise.main/Program.cs +++ b/exercise.main/Program.cs @@ -1,2 +1,229 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using exercise.main; +using exercise.main.Inventory; +using exercise.main.Products; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace exercise.main +{ + public class Program + { + public static void Main() + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + + var basket = new Basket(); + var catalog = new Catalog(); + + while (true) + { + Console.WriteLine("Welcome to Bob's Bagels! What would you like to order?"); + Console.WriteLine("1. Bagel"); + Console.WriteLine("2. Coffee"); + Console.WriteLine("(q = quit)"); + Console.Write("Choice: "); + var first = (Console.ReadLine() ?? "").Trim(); + if (first.Equals("q", StringComparison.OrdinalIgnoreCase)) return; + + if (first == "1") + { + AddBagelWithFillings(catalog, basket); + } + else if (first == "2") + { + AddCoffee(catalog, basket); + } + else + { + Console.WriteLine("Invalid choice.\n"); + continue; + } + + // change menu + while (true) + { + Console.WriteLine("\nDo you want to change anything in your basket?"); + Console.WriteLine("1. Add something"); + Console.WriteLine("2. Remove something"); + Console.WriteLine("3. No, I'm good"); + Console.Write("Choice: "); + var change = (Console.ReadLine() ?? "").Trim(); + + if (change == "1") + { + Console.WriteLine("\nWhat would you like to add?"); + Console.WriteLine("1. Bagel"); + Console.WriteLine("2. Coffee"); + Console.WriteLine("3. Cancel"); + Console.Write("Choice: "); + var add = (Console.ReadLine() ?? "").Trim(); + + if (add == "1") AddBagelWithFillings(catalog, basket); + else if (add == "2") AddCoffee(catalog, basket); + } + else if (change == "2") + { + if (!RemoveByPrintedNumber(basket)) + continue; // invalid number -> message already shown + } + else if (change == "3") break; + else Console.WriteLine("Invalid choice."); + } + + // summary and exit + PrintBasketIndented(basket); + Console.WriteLine($"\nYour total is: ${basket.TotalPrice:0.00}\n"); + Console.WriteLine("Thank you for your order!"); + return; + } + } + + // ---------- flows ---------- + private static void AddBagelWithFillings(ICatalog catalog, Basket basket) + { + var bagel = PickOne(catalog, ProductType.Bagel, "Which bagel would you like?"); + if (bagel == null) return; + + basket.AddProduct(catalog.CreateProduct(bagel.Sku)); + + var chosenFillings = PickFillings(catalog, bagel.Variant); + foreach (var f in chosenFillings) basket.AddProduct(catalog.CreateProduct(f.Sku)); + + Console.WriteLine(chosenFillings.Count == 0 + ? $"\nYou have ordered a {bagel.Variant.ToLower()} bagel with no fillings." + : $"\nYou have ordered a {bagel.Variant.ToLower()} bagel with {FormatList(chosenFillings.Select(x => x.Variant))}."); + } + + private static void AddCoffee(ICatalog catalog, Basket basket) + { + var coffee = PickOne(catalog, ProductType.Coffee, "Which coffee would you like?"); + if (coffee == null) return; + + basket.AddProduct(catalog.CreateProduct(coffee.Sku)); + Console.WriteLine($"\nYou have added {coffee.Variant.ToLower()} coffee."); + } + + // ---------- pickers ---------- + private static CatalogItem? PickOne(ICatalog catalog, ProductType type, string title) + { + var avail = catalog.GetByType(type, onlyInStock: true).ToList(); + var sold = catalog.GetSoldOutByType(type).ToList(); + + Console.WriteLine($"\n{title}"); + for (int i = 0; i < avail.Count; i++) Console.WriteLine($"{i + 1}. {avail[i].Variant} — ${avail[i].Price:0.00}"); + if (sold.Count > 0) Console.WriteLine($"\nWe are unfortunately out of stock for: {string.Join(", ", sold.Select(x => x.Variant))}"); + Console.Write("Enter number: "); + + int idx = ReadIndex(avail.Count); + return avail[idx]; + } + + private static List PickFillings(ICatalog catalog, string bagelVariant) + { + var fills = catalog.GetByType(ProductType.Filling, onlyInStock: true).ToList(); + Console.WriteLine($"\nWhat would you like on your {bagelVariant.ToLower()} bagel?"); + for (int i = 0; i < fills.Count; i++) Console.WriteLine($"{i + 1}. {fills[i].Variant} — ${fills[i].Price:0.00}"); + Console.WriteLine($"{fills.Count + 1}. No fillings"); + Console.Write($"Pick one or more (e.g., 1,3) or {fills.Count + 1} for none: "); + + var input = (Console.ReadLine() ?? "").Trim(); + if (int.TryParse(input, out var single) && single == fills.Count + 1) return new(); + + var chosen = new List(); + foreach (var p in input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (int.TryParse(p, out var n) && n >= 1 && n <= fills.Count) + if (!chosen.Contains(fills[n - 1])) chosen.Add(fills[n - 1]); + + return chosen; + } + + // ---------- basket UI ---------- + private static void PrintBasketIndented(Basket basket) + { + Console.WriteLine("\nYour basket:"); + for (int i = 0; i < basket.Products.Count; i++) + { + var p = basket.Products[i]; + if (p.Type == ProductType.Bagel) + { + Console.WriteLine($"- {p.Name} - {p.Variant} … ${p.Price:0.00}"); + int j = i + 1; + while (j < basket.Products.Count && basket.Products[j].Type == ProductType.Filling) + { + var f = basket.Products[j]; + Console.WriteLine($" - {f.Name} - {f.Variant} … ${f.Price:0.00}"); + j++; + } + i = j - 1; + } + else + { + Console.WriteLine($"- {p.Name} - {p.Variant} … ${p.Price:0.00}"); + } + } + } + + private static bool RemoveByPrintedNumber(Basket basket) + { + if (basket.Products.Count == 0) { Console.WriteLine("\nYour basket is empty."); return false; } + + Console.WriteLine("\nYour basket (choose a number to remove):"); + for (int i = 0; i < basket.Products.Count; i++) + { + var p = basket.Products[i]; + var indent = p.Type == ProductType.Filling ? " " : ""; + Console.WriteLine($"{i + 1}. {indent}{p.Name} - {p.Variant} … ${p.Price:0.00}"); + } + Console.Write("Enter number to remove: "); + var raw = (Console.ReadLine() ?? "").Trim(); + + // foreach-based validation (ok per your preference) + var found = false; int choice; + if (int.TryParse(raw, out choice)) + { + int pos = 1; + foreach (var _ in basket.Products) { if (pos == choice) { found = true; break; } pos++; } + } + if (!found) { Console.WriteLine("That item is not in your basket."); return false; } + + int idx = choice - 1; + var item = basket.Products[idx]; + + if (item.Type == ProductType.Bagel) + { + basket.Products.RemoveAt(idx); + while (idx < basket.Products.Count && basket.Products[idx].Type == ProductType.Filling) + basket.Products.RemoveAt(idx); + Console.WriteLine("\nRemoved bagel (and its fillings)."); + } + else + { + basket.Products.RemoveAt(idx); + Console.WriteLine("\nRemoved item."); + } + return true; + } + + private static int ReadIndex(int max) + { + while (true) + { + var input = (Console.ReadLine() ?? "").Trim(); + if (int.TryParse(input, out var n) && n >= 1 && n <= max) return n - 1; + Console.Write("Invalid choice. Try again: "); + } + } + + private static string FormatList(IEnumerable items) + { + var list = items.Select(s => s.ToLower()).ToList(); + if (list.Count == 0) return ""; + if (list.Count == 1) return list[0]; + if (list.Count == 2) return $"{list[0]} and {list[1]}"; + return string.Join(", ", list.Take(list.Count - 1)) + " and " + list[^1]; + } + } +} diff --git a/exercise.main/domain_model.md b/exercise.main/domain_model.md new file mode 100644 index 00000000..564679e9 --- /dev/null +++ b/exercise.main/domain_model.md @@ -0,0 +1,30 @@ +# Domain Modelling + +## Core Tasks + +1. Add items to basket +2. Remove items from basket +3. Notify when basket is full +4. Change basket capacity +5. Handle removing items that don’t exist in basket +6. View total cost of basket +7. View price of item before adding to basket +8. Add extra fillings to bagels +9. View price of fillings +10. Only order items that are in stock + + +| # | Classes/Interfaces | Methods/Properties | Scenario | Outputs | +| -- | ------------------ | ----------------------------- | --------------------------------------------- | ---------------------------- | +| 1 | Basket | AddItem() | Add a bagel or other product to basket | Item added | +| 2 | IBasket | RemoveItem() | Remove product from basket | Item removed | +| 3 | | AddItem(...) ➜ capacity check | Notify when basket is full | BasketFullException | +| 4 | | SetCapacity() | Change the basket capacity | Updated capacity | +| 5 | IBasket | RemoveItem() | Attempt to remove item that doesn’t exist | ItemNotInBasketException | +| 6 | Basket | Totalprice() | Get total price of all items in basket | Decimal sum | +| 7 | ICatalog | GetProduct().Price | View price of item before adding it to basket | Decimal price | +| 8 | Basket | AddItem() | Choose extra fillings for bagel | Item with fillings | +| 9 | ICatalog | GetProduct().Price | View price of each filling | Decimal price | +| 10 | ICatalog | Has(sku) | Only allow adding items that exist in stock | OK / NotInInventoryException | + + diff --git a/exercise.tests/UnitTest1.cs b/exercise.tests/UnitTest1.cs index 7bdb8968..1ac5af8e 100644 --- a/exercise.tests/UnitTest1.cs +++ b/exercise.tests/UnitTest1.cs @@ -1,15 +1,77 @@ +using static NUnit.Framework.Internal.OSPlatform; +using exercise.main; +using exercise.main.Inventory; +using ProductType = exercise.main.Products.ProductType; + + namespace exercise.tests; public class Tests { - [SetUp] - public void Setup() + [Test] + public void AddToBasket_Bagel() { + var catalog = new Catalog(); + var basket = new Basket(); + + var bagel = catalog.CreateProduct("BGLP"); + basket.AddProduct(bagel); + + Assert.That(basket.Products.Count, Is.EqualTo(1)); + Assert.That(basket.Products[0].Type, Is.EqualTo(ProductType.Bagel)); + Assert.That(basket.Products[0].Variant, Is.EqualTo("Plain")); } [Test] - public void Test1() + public void AddToBasket_OutOfStock() { - Assert.Pass(); + var catalog = new Catalog(); + var basket = new Basket(); + + Assert.That(catalog.Has("COFC"), Is.True); + + // trying to create the product should fail + Assert.Throws(() => catalog.CreateProduct("COFC")); + + // nothing was added + Assert.That(basket.Products, Is.Empty); + } + + [Test] + public void RemoveFromBasket() + { + var catalog = new Catalog(); + var basket = new Basket(); + + basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("COFB")); + + basket.Products.RemoveAt(0); + + Assert.That(basket.Products.Count, Is.EqualTo(1)); + } + /* + [Test] + public void RemoveItemsNotInBasket() + { + var catalog = new Catalog(); + var basket = new Basket(); + + basket.AddProduct(catalog.CreateProduct("BGLP")); + + + } + */ + [Test] + public void TotalCost() + { + var catalog = new Catalog(); + var basket = new Basket(); + + basket.AddProduct(catalog.CreateProduct("BGLP")); // 0.39 + basket.AddProduct(catalog.CreateProduct("COFB")); // 0.99 (in stock) + basket.AddProduct(catalog.CreateProduct("FILC")); // 0.12 + + Assert.That(basket.TotalPrice, Is.EqualTo(0.39m + 0.99m + 0.12m)); // 1.50 } -} \ 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 @@ + + + + From f6be6ac9e35ced37d8bb4ca4b26b76270b741647 Mon Sep 17 00:00:00 2001 From: Mona Eikli Andresen Date: Tue, 12 Aug 2025 16:00:02 +0200 Subject: [PATCH 2/3] Mona Eikli --- exercise.main/Basket.cs | 37 +++++- exercise.main/Inventory/Catalog.cs | 89 +++++++++++--- exercise.main/Inventory/ICatalog.cs | 4 +- exercise.main/Program.cs | 31 +++-- exercise.tests/UnitTest1.cs | 184 +++++++++++++++++++++++++--- 5 files changed, 291 insertions(+), 54 deletions(-) diff --git a/exercise.main/Basket.cs b/exercise.main/Basket.cs index 2a318dab..5aa3001e 100644 --- a/exercise.main/Basket.cs +++ b/exercise.main/Basket.cs @@ -1,4 +1,5 @@ -using exercise.main.Products; +using exercise.main.Inventory; +using exercise.main.Products; using System; using System.Collections.Generic; using System.Linq; @@ -11,8 +12,40 @@ namespace exercise.main public class Basket { private readonly List _items = new(); + private int _capacity; - public void AddProduct(IProduct product) => _items.Add(product); + public Basket(int capacity = 4) + { + _capacity = capacity; + } + + public int Capacity { get { return _capacity; } set { _capacity = value; } } + + public string AddProduct(IProduct product) + { + if(_items.Count >= _capacity) + { + return $"the basket is full"; + } else + { + _items.Add(product); + return $"Item {product.Name} was successfully added to the basket."; + } + } + + // NEW: add by SKU with in-stock check (uses Catalog) + public string AddProduct(ICatalog catalog, string sku) + { + if (catalog == null) return "no catalog"; + if (!catalog.ProductExists(sku)) return "item not found"; + + var item = catalog.Get(sku); + if (!item.InStock) return $"{item.Name} - {item.Variant} is out of stock."; + + // create & add (capacity check + message reused) + var product = catalog.CreateProduct(sku); + return AddProduct(product); + } public List Products => _items; diff --git a/exercise.main/Inventory/Catalog.cs b/exercise.main/Inventory/Catalog.cs index 72a21c88..19d0bece 100644 --- a/exercise.main/Inventory/Catalog.cs +++ b/exercise.main/Inventory/Catalog.cs @@ -35,33 +35,88 @@ public Catalog() }; } - public bool Has(string sku) => _items.ContainsKey(sku); + public bool ProductExists(string sku) + { + return _items.ContainsKey(sku); + } + + public CatalogItem Get(string sku) + { + CatalogItem item; + if (_items.TryGetValue(sku, out item)) + { + return item; + } + return null; + } - public CatalogItem Get(string sku) => - _items.TryGetValue(sku, out var item) ? item - : throw new KeyNotFoundException($"SKU '{sku}' not found."); + // Returns all items of a specific type, optionally filtering out those that are not in stock. + public IEnumerable GetByType(ProductType type, bool onlyInStock = true) + { + var result = new List(); + + foreach (var i in _items.Values) + { + if (i.Type == type) + { + if (onlyInStock && !i.InStock) + { + continue; + } + result.Add(i); + } + } - public IEnumerable GetByType(ProductType type, bool onlyInStock = true) => - _items.Values.Where(i => i.Type == type && (!onlyInStock || i.InStock)); + return result; + } - public IEnumerable GetSoldOutByType(ProductType type) => - _items.Values.Where(i => i.Type == type && !i.InStock); + // Returns all sold-out items of a specific type. + public IEnumerable GetSoldOutByType(ProductType type) + { + var result = new List(); + foreach (var i in _items.Values) + { + if (i.Type == type && !i.InStock) + { + result.Add(i); + } + } + + return result; + } + + // Creates a product instance based on the SKU. + // Returns null if the SKU doesn't exist, the item is out of stock, or the type is unsupported. public IProduct CreateProduct(string sku) { - var i = Get(sku); - if (!i.InStock) throw new InvalidOperationException($"{i.Name} - {i.Variant} is out of stock."); - var id = _nextId++; + if (!_items.TryGetValue(sku, out var item)) + { + // SKU not found + return null; + } - return i.Type switch + // Not in stock + if (!item.InStock) { - ProductType.Bagel => new Bagel(id, i.Sku, i.Variant, i.Price), - ProductType.Coffee => new Coffee(id, i.Sku, i.Variant, i.Price), - ProductType.Filling => new Filling(id, i.Sku, i.Variant, i.Price), - _ => throw new NotSupportedException($"Unsupported type for SKU '{i.Sku}'.") - }; + return null; + } + + // Unique id in case of multible of the same sku + var id = _nextId++; + + // Create the correct product type + if (item.Type == ProductType.Bagel) + return new Bagel(id, item.Sku, item.Variant, item.Price); + if (item.Type == ProductType.Coffee) + return new Coffee(id, item.Sku, item.Variant, item.Price); + if (item.Type == ProductType.Filling) + return new Filling(id, item.Sku, item.Variant, item.Price); + + return null; } + public decimal GetPrice(string sku) => Get(sku).Price; } } diff --git a/exercise.main/Inventory/ICatalog.cs b/exercise.main/Inventory/ICatalog.cs index 730977a6..da2b4d5e 100644 --- a/exercise.main/Inventory/ICatalog.cs +++ b/exercise.main/Inventory/ICatalog.cs @@ -9,11 +9,11 @@ namespace exercise.main.Inventory { public interface ICatalog { - bool Has(string sku); + bool ProductExists(string sku); CatalogItem Get(string sku); IEnumerable GetByType(ProductType type, bool onlyInStock = true); IEnumerable GetSoldOutByType(ProductType type); - IProduct CreateProduct(string sku); // makes a concrete Bagel/Coffee/Filling + IProduct CreateProduct(string sku); decimal GetPrice(string sku); } } diff --git a/exercise.main/Program.cs b/exercise.main/Program.cs index 86222488..4ac66f89 100644 --- a/exercise.main/Program.cs +++ b/exercise.main/Program.cs @@ -42,7 +42,7 @@ public static void Main() continue; } - // change menu + // Change your order menu while (true) { Console.WriteLine("\nDo you want to change anything in your basket?"); @@ -67,13 +67,13 @@ public static void Main() else if (change == "2") { if (!RemoveByPrintedNumber(basket)) - continue; // invalid number -> message already shown + continue; } else if (change == "3") break; else Console.WriteLine("Invalid choice."); } - // summary and exit + // Summary and exit PrintBasketIndented(basket); Console.WriteLine($"\nYour total is: ${basket.TotalPrice:0.00}\n"); Console.WriteLine("Thank you for your order!"); @@ -81,7 +81,7 @@ public static void Main() } } - // ---------- flows ---------- + // Adds a bagel with optional fillings to the basket private static void AddBagelWithFillings(ICatalog catalog, Basket basket) { var bagel = PickOne(catalog, ProductType.Bagel, "Which bagel would you like?"); @@ -97,6 +97,7 @@ private static void AddBagelWithFillings(ICatalog catalog, Basket basket) : $"\nYou have ordered a {bagel.Variant.ToLower()} bagel with {FormatList(chosenFillings.Select(x => x.Variant))}."); } + // Adds a coffee to the basket private static void AddCoffee(ICatalog catalog, Basket basket) { var coffee = PickOne(catalog, ProductType.Coffee, "Which coffee would you like?"); @@ -106,24 +107,26 @@ private static void AddCoffee(ICatalog catalog, Basket basket) Console.WriteLine($"\nYou have added {coffee.Variant.ToLower()} coffee."); } - // ---------- pickers ---------- + // pick one item from the printed catalog private static CatalogItem? PickOne(ICatalog catalog, ProductType type, string title) { - var avail = catalog.GetByType(type, onlyInStock: true).ToList(); - var sold = catalog.GetSoldOutByType(type).ToList(); + var ItemsInStock = catalog.GetByType(type, onlyInStock: true).ToList(); + var ItemsOutOfStock = catalog.GetSoldOutByType(type).ToList(); Console.WriteLine($"\n{title}"); - for (int i = 0; i < avail.Count; i++) Console.WriteLine($"{i + 1}. {avail[i].Variant} — ${avail[i].Price:0.00}"); - if (sold.Count > 0) Console.WriteLine($"\nWe are unfortunately out of stock for: {string.Join(", ", sold.Select(x => x.Variant))}"); + for (int i = 0; i < ItemsInStock.Count; i++) Console.WriteLine($"{i + 1}. {ItemsInStock[i].Variant} — ${ItemsInStock[i].Price:0.00}"); + if (ItemsOutOfStock.Count > 0) Console.WriteLine($"\nWe are unfortunately out of stock for: {string.Join(", ", ItemsOutOfStock.Select(x => x.Variant))}"); Console.Write("Enter number: "); - int idx = ReadIndex(avail.Count); - return avail[idx]; + int idx = ReadIndex(ItemsInStock.Count); + return ItemsInStock[idx]; } + // Pick one or more fillings for the bagel, returns a list of CatalogItems private static List PickFillings(ICatalog catalog, string bagelVariant) { var fills = catalog.GetByType(ProductType.Filling, onlyInStock: true).ToList(); + Console.WriteLine($"\nWhat would you like on your {bagelVariant.ToLower()} bagel?"); for (int i = 0; i < fills.Count; i++) Console.WriteLine($"{i + 1}. {fills[i].Variant} — ${fills[i].Price:0.00}"); Console.WriteLine($"{fills.Count + 1}. No fillings"); @@ -132,6 +135,7 @@ private static List PickFillings(ICatalog catalog, string bagelVari var input = (Console.ReadLine() ?? "").Trim(); if (int.TryParse(input, out var single) && single == fills.Count + 1) return new(); + // Check if there are more than one filling selected var chosen = new List(); foreach (var p in input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) if (int.TryParse(p, out var n) && n >= 1 && n <= fills.Count) @@ -140,7 +144,7 @@ private static List PickFillings(ICatalog catalog, string bagelVari return chosen; } - // ---------- basket UI ---------- + // Printing out the basket with indentation for fillings private static void PrintBasketIndented(Basket basket) { Console.WriteLine("\nYour basket:"); @@ -180,7 +184,6 @@ private static bool RemoveByPrintedNumber(Basket basket) Console.Write("Enter number to remove: "); var raw = (Console.ReadLine() ?? "").Trim(); - // foreach-based validation (ok per your preference) var found = false; int choice; if (int.TryParse(raw, out choice)) { @@ -207,6 +210,7 @@ private static bool RemoveByPrintedNumber(Basket basket) return true; } + // Reads the input and ensuring it is valid based on the choices available. private static int ReadIndex(int max) { while (true) @@ -217,6 +221,7 @@ private static int ReadIndex(int max) } } + // Formats the list of items for output depending on how many items there are. private static string FormatList(IEnumerable items) { var list = items.Select(s => s.ToLower()).ToList(); diff --git a/exercise.tests/UnitTest1.cs b/exercise.tests/UnitTest1.cs index 1ac5af8e..1899afc9 100644 --- a/exercise.tests/UnitTest1.cs +++ b/exercise.tests/UnitTest1.cs @@ -2,6 +2,7 @@ using exercise.main; using exercise.main.Inventory; using ProductType = exercise.main.Products.ProductType; +using exercise.main.Products; namespace exercise.tests; @@ -23,55 +24,198 @@ public void AddToBasket_Bagel() } [Test] - public void AddToBasket_OutOfStock() + public void RemoveFromBasket() { - var catalog = new Catalog(); + var catalog = new Catalog(); var basket = new Basket(); - Assert.That(catalog.Has("COFC"), Is.True); + basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("COFB")); + + basket.Products.RemoveAt(0); + + Assert.That(basket.Products.Count, Is.EqualTo(1)); + } + + [Test] + public void TotalCost() + { + var catalog = new Catalog(); + var basket = new Basket(); - // trying to create the product should fail - Assert.Throws(() => catalog.CreateProduct("COFC")); + basket.AddProduct(catalog.CreateProduct("BGLP")); // 0.39 + basket.AddProduct(catalog.CreateProduct("COFB")); // 0.99 + basket.AddProduct(catalog.CreateProduct("FILC")); // 0.12 - // nothing was added - Assert.That(basket.Products, Is.Empty); + Assert.That(basket.TotalPrice, Is.EqualTo(0.39m + 0.99m + 0.12m)); // 1.50 } [Test] - public void RemoveFromBasket() + public void IfBasketIsFullMessageShouldBe() { var catalog = new Catalog(); var basket = new Basket(); - basket.AddProduct(catalog.CreateProduct("BGLP")); + + basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("BGLP")); + + string resultat = basket.AddProduct(catalog.CreateProduct("BGLO")); + + string expected = $"the basket is full"; + + Assert.That(resultat, Is.EqualTo(expected)); + } + + [Test] + public void ChangeBasketCapacity() + { + var catalog = new Catalog(); + var basket = new Basket(2); + + Assert.That(basket.Capacity, Is.EqualTo(2)); + + basket.AddProduct(catalog.CreateProduct("BGLP")); basket.AddProduct(catalog.CreateProduct("COFB")); + basket.AddProduct(catalog.CreateProduct("FILC")); - basket.Products.RemoveAt(0); + string resultat = basket.AddProduct(catalog.CreateProduct("BGLO")); + + string expected = $"the basket is full"; + Assert.That(resultat, Is.EqualTo(expected)); + + basket.Capacity = 8; // change capacity to 8 + + string result = basket.AddProduct(catalog.CreateProduct("BGLO")); + + string expected2 = $"Item {catalog.CreateProduct("BGLO").Name} was successfully added to the basket."; + + Assert.That(basket.Capacity, Is.EqualTo(8)); + Assert.That(result, Is.EqualTo(expected2)); + } + + [Test] + public void ViewPriceOfItem() + { + var catalog = new Catalog(); + var basket = new Basket(); + basket.AddProduct(catalog.CreateProduct("BGLP")); // 0.39 + basket.AddProduct(catalog.CreateProduct("COFB")); // 0.99 + basket.AddProduct(catalog.CreateProduct("FILC")); // 0.12 + + Assert.That(basket.Products[0].Price, Is.EqualTo(0.39m)); + Assert.That(basket.Products[1].Price, Is.EqualTo(0.99m)); + Assert.That(basket.Products[2].Price, Is.EqualTo(0.12m)); + } + + [Test] + public void OnlyOrderItemsInStock() + { + var catalog = new Catalog(); + var basket = new Basket(); + + // in-stock -> added + string expected = "Item Bagel was successfully added to the basket."; + string result = basket.AddProduct(catalog, "BGLP"); + Assert.That(result, Is.EqualTo(expected)); Assert.That(basket.Products.Count, Is.EqualTo(1)); + + // try add out-of-stock item + var outOfStockItem = catalog.Get("COFC"); + expected = $"{outOfStockItem.Name} - {outOfStockItem.Variant} is out of stock."; + result = basket.AddProduct(catalog, "COFC"); + + Assert.That(result, Is.EqualTo(expected)); + Assert.That(basket.Products.Count, Is.EqualTo(1)); } - /* + [Test] - public void RemoveItemsNotInBasket() + public void AddExtraFillingsToBagel() { var catalog = new Catalog(); var basket = new Basket(); - basket.AddProduct(catalog.CreateProduct("BGLP")); + basket.AddProduct(catalog.CreateProduct("BGLP")); // bagel + + string expected = "Item Filling was successfully added to the basket."; - + string result = basket.AddProduct(catalog.CreateProduct("FILC")); // cheese + Assert.That(result, Is.EqualTo(expected)); + + result = basket.AddProduct(catalog.CreateProduct("FILB")); // bacon + Assert.That(result, Is.EqualTo(expected)); + + Assert.That(basket.Products.Count, Is.EqualTo(3)); + Assert.That(basket.Products[1].Type, Is.EqualTo(ProductType.Filling)); + Assert.That(basket.Products[2].Type, Is.EqualTo(ProductType.Filling)); } - */ + [Test] - public void TotalCost() + public void AddFillings() { var catalog = new Catalog(); var basket = new Basket(); - basket.AddProduct(catalog.CreateProduct("BGLP")); // 0.39 - basket.AddProduct(catalog.CreateProduct("COFB")); // 0.99 (in stock) - basket.AddProduct(catalog.CreateProduct("FILC")); // 0.12 + basket.AddProduct(catalog.CreateProduct("BGLP")); // bagel - Assert.That(basket.TotalPrice, Is.EqualTo(0.39m + 0.99m + 0.12m)); // 1.50 + string expected = "Item Filling was successfully added to the basket."; + string result = basket.AddProduct(catalog.CreateProduct("FILC")); + Assert.That(result, Is.EqualTo(expected)); + + result = basket.AddProduct(catalog.CreateProduct("FILB")); // bacon + Assert.That(result, Is.EqualTo(expected)); + + Assert.That(basket.Products.Count, Is.EqualTo(3)); + Assert.That(basket.Products.Count(p => p.Type == ProductType.Filling), Is.EqualTo(2)); + } + + [Test] + public void ViewPriceOfEachFilling() + { + var catalog = new Catalog(); + + var expected = new Dictionary + { + ["FILB"] = 0.12m, // Bacon + ["FILE"] = 0.12m, // Egg + ["FILC"] = 0.12m, // Cheese + ["FILX"] = 0.12m, // Cream Cheese + ["FILS"] = 0.12m, // Smoked Salmon + ["FILH"] = 0.12m, // Ham + }; + + foreach (var (sku, expectedPrice) in expected) + { + var result = catalog.GetPrice(sku); + Assert.That(result, Is.EqualTo(expectedPrice)); + } } + + [Test] + public void ViewPriceOfEachBagel() + { + var catalog = new Catalog(); + + var expected = new Dictionary + { + ["BGLO"] = 0.49m, // Onion + ["BGLP"] = 0.39m, // Plain + ["BGLE"] = 0.49m, // Everything + ["BGLS"] = 0.49m, // Sesame + }; + + foreach (var entry in expected) + { + decimal expectedPrice = entry.Value; + decimal result = catalog.GetPrice(entry.Key); + Assert.That(result, Is.EqualTo(expectedPrice)); + } + } + + + + } From 1bed2864e361313c795df0fd6f78508d15d65528 Mon Sep 17 00:00:00 2001 From: Mona Eikli Andresen Date: Wed, 13 Aug 2025 10:33:41 +0200 Subject: [PATCH 3/3] Mona Eikli --- exercise.main/Basket.cs | 2 +- exercise.main/domain_model.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/exercise.main/Basket.cs b/exercise.main/Basket.cs index 5aa3001e..c5f5c97b 100644 --- a/exercise.main/Basket.cs +++ b/exercise.main/Basket.cs @@ -33,7 +33,7 @@ public string AddProduct(IProduct product) } } - // NEW: add by SKU with in-stock check (uses Catalog) + // add by SKU with in-stock check public string AddProduct(ICatalog catalog, string sku) { if (catalog == null) return "no catalog"; diff --git a/exercise.main/domain_model.md b/exercise.main/domain_model.md index 564679e9..dd0bf3f6 100644 --- a/exercise.main/domain_model.md +++ b/exercise.main/domain_model.md @@ -16,15 +16,15 @@ | # | Classes/Interfaces | Methods/Properties | Scenario | Outputs | | -- | ------------------ | ----------------------------- | --------------------------------------------- | ---------------------------- | -| 1 | Basket | AddItem() | Add a bagel or other product to basket | Item added | -| 2 | IBasket | RemoveItem() | Remove product from basket | Item removed | -| 3 | | AddItem(...) ➜ capacity check | Notify when basket is full | BasketFullException | -| 4 | | SetCapacity() | Change the basket capacity | Updated capacity | +| 1 | Basket | AddItem() | Add a bagel or other product to basket | Item added | +| 2 | Basket | RemoveItem() | Remove product from basket | Item removed | +| 3 | Basket | AddItem() - capacity check | Notify when basket is full | BasketFullException | +| 4 | Basket | SetCapacity() | Change the basket capacity | Updated capacity | | 5 | IBasket | RemoveItem() | Attempt to remove item that doesn’t exist | ItemNotInBasketException | -| 6 | Basket | Totalprice() | Get total price of all items in basket | Decimal sum | +| 6 | Basket | Totalprice() | Get total price of all items in basket | Decimal sum | | 7 | ICatalog | GetProduct().Price | View price of item before adding it to basket | Decimal price | -| 8 | Basket | AddItem() | Choose extra fillings for bagel | Item with fillings | +| 8 | Basket | AddItem() | Choose extra fillings for bagel | Filling added | | 9 | ICatalog | GetProduct().Price | View price of each filling | Decimal price | -| 10 | ICatalog | Has(sku) | Only allow adding items that exist in stock | OK / NotInInventoryException | +| 10 | ICatalog | ProductExists | Only allow adding items that exist in stock | OK / NotInInventoryException |