Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f0ce731
Added test for adding a bagel to the basket
MathiasHandeland Aug 11, 2025
1b03399
added tests for removing bagel from basket
MathiasHandeland Aug 11, 2025
8bbf06a
add bagel to basket test works now
MathiasHandeland Aug 11, 2025
7a43655
test for removing a bagel from basket works
MathiasHandeland Aug 11, 2025
8260f0b
added tests and source code for basket capacity
MathiasHandeland Aug 11, 2025
d1f5819
added test for basket capacity and implented the source code for this
MathiasHandeland Aug 11, 2025
65b65e5
updated domain model for user story 1-3
MathiasHandeland Aug 11, 2025
65fe2ad
added test for removing a product that is not present in basket
MathiasHandeland Aug 11, 2025
cccf202
added expection to RemoveProduct method to not allow removing a produ…
MathiasHandeland Aug 11, 2025
9c46a68
updated domain model for user story 6
MathiasHandeland Aug 11, 2025
acb5b6d
implemented source code for basket total
MathiasHandeland Aug 11, 2025
2d495e3
updated the domain model for user story 7
MathiasHandeland Aug 11, 2025
2bea04f
implemented test and source code for user story 7 (bagel price)
MathiasHandeland Aug 11, 2025
e483f5b
Update domainModel.md
MathiasHandeland Aug 11, 2025
452abec
added test for user case 8 (filling)
MathiasHandeland Aug 11, 2025
a9518a8
implemented choose bagel filling test and source code and update doma…
MathiasHandeland Aug 11, 2025
95bd0d8
updated domain model for user story 9 (filling price)
MathiasHandeland Aug 11, 2025
7a10b72
added test for user story 9 price of fillings
MathiasHandeland Aug 11, 2025
262bb28
finished adding tests for user story 10 and implemented source code
MathiasHandeland Aug 11, 2025
bd88627
made it possible to add multiple fillings to a bagel, and updated tes…
MathiasHandeland Aug 11, 2025
e9a1d5d
updated Fillings class and implemented Coffee class
MathiasHandeland Aug 11, 2025
2806e72
updated domain model
MathiasHandeland Aug 12, 2025
666c8fc
made code cleaner
MathiasHandeland Aug 12, 2025
f3d45d3
written domain model description for receipt extension and added simp…
MathiasHandeland Aug 12, 2025
5656cd9
implemented Receipt class and Print method. made the printing more es…
MathiasHandeland Aug 12, 2025
8381527
added test for printing receipts: checking that something was actuall…
MathiasHandeland Aug 12, 2025
bb4c10f
used CultureInfo.InvariantCulture to make it say £12.71 and not £12,71
MathiasHandeland Aug 12, 2025
3bc9b62
finished domain model description for receipt and finished implementi…
MathiasHandeland Aug 12, 2025
a6558dc
updated domain model for discount extention
MathiasHandeland Aug 12, 2025
0214605
started on tests for discount extention
MathiasHandeland Aug 12, 2025
59ca76b
added test, updated domain model and source code for discount extention
MathiasHandeland Aug 12, 2025
eab86e6
the receipt is now updated and shows subtotal and total with discount…
MathiasHandeland Aug 12, 2025
039e92e
added new test for discount. chechking that no discount is added for …
MathiasHandeland Aug 12, 2025
26805b4
small change to domain model for discount extention
MathiasHandeland Aug 12, 2025
5b360fe
changed format of DateTime tostring and made some nice touches to the…
MathiasHandeland Aug 12, 2025
a35918f
added non implemented test for twilio receipt printer and implemented…
MathiasHandeland Aug 12, 2025
df1bc42
updated domain model for twilio extention
MathiasHandeland Aug 12, 2025
3bab7e5
bobs bagels finito
MathiasHandeland Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions domainModel.md
Original file line number Diff line number Diff line change
@@ -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<Filling> 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<string, (string Name, string Variant, int Quantity, decimal Subtotal)> |
| 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 |
122 changes: 122 additions & 0 deletions exercise.main/Basket.cs
Original file line number Diff line number Diff line change
@@ -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<IProduct> _basketItems = new List<IProduct>();
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<IProduct> 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.");
}
}

}

/// <summary>
/// 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
/// </summary>
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<Bagel>() // 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);
}
}
}
80 changes: 80 additions & 0 deletions exercise.main/ConsoleReceiptPrinter.cs
Original file line number Diff line number Diff line change
@@ -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<string, (string Name, string Variant, int Quantity, decimal Subtotal)> orderDict =
new Dictionary<string, (string Name, string Variant, int Quantity, decimal Subtotal)>();

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}");
}
}
}
13 changes: 13 additions & 0 deletions exercise.main/IReceiptPrinter.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
26 changes: 26 additions & 0 deletions exercise.main/Inventory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace exercise.main
{
/// <summary>
/// Ensures that customers are only able to order things that are stocked in inventory.
/// </summary>
public class Inventory
{
private HashSet<string> _inventory = new HashSet<string>()
{
"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);
}
}
}
Loading