Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 109 additions & 0 deletions domain-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Domain Modelling

## Interfaces

### IInventoryProduct

| Property/Method | Scenario | Returns |
| --------------- | --------------------------------------------------------- | ------- |
| Name | Full item name | string |
| SKU | Item code | string |
| Price | Undiscounted base price (with no fillings) | Decimal |
| GetFinalPrice() | Returns the final item price, with discounts and fillings | Decimal |

### IDiscountable

| Property / Methods | Scenario | Returns |
| ------------------------------- | --------------------------------------------------------------- | ------- |
| DiscountedPrice | price after discounts | Decimal |
| IsDiscounted | is item discounted boolean | bool |
| SetDiscountPrice(Decimal price) | sets the discount price | |
| GetSavedAmount() | returns the difference between<br>original and discounted price | Decimal |

### IFillable

| Property / Method | Scenario | Returns |
| --------------------------- | -------------------------------- | -------------- |
| Fillings | stores all filling on an item | List\<Fillings> |
| AddFilling(Filling filling) | add a filling to a fillable item | |

### IReceiptPrinter

| Method | Scenario |
| --------------------- | ------------------ |
| Print(string receipt) | Prints the receipt |


## Inheriting Classes

### Bagel : IInventoryProduct, IDiscountable, IFillable
### Coffee : IInventoryProduct, IDiscountable
### Filling : IInventoryProduct

### ConsoleReceiptPrinter : IReceiptPrinter
### TwilioReceiptPrinter : IReceiptPrinter

### BasketFullException : Exception
### ItemNotInBasketException : Exception


## Other classes

### Basket

| Property / method | Scenario | Returns |
| ------------------------------------- | ---------------------------------------------------------------------- | --------------------- |
| Capacity | shows basket's capacity | int |
| Products | shows the content of the basket | List\<IDiscountable> |
| Remove(string SKU) | remove product in the basket | |
| ChangeCapacity(int capacity) | Changes basket's capacity | |
| GetTotalCost() | Get Final cost of all items in the basket | Decimal |
| PrintReceipt(IReceiptPrinter printer) | prints receipt | |
| private CalculateDiscounts() | calculates discounts when adding and removing products from the basket | |

### Discount

| Property / Method | Scenario | Returns |
| ------------------------- | ------------------------------------------------------------------------------------------- | --------------------------- |
| ItemRequirementAmountDict | stores requirements for a discount to be eligible,<br>in a format of <SKU, required amount> | Dictionary<string, int> |
| ItemDiscountedPriceDict | stores discounted price for item, mapped to SKU | Dictionary<string, Decimal> |
| SavedAmount | Amount the discount saved | Decimal |
| CalculateSavedAmount() | Calculated saved amount


## Static classes

### Discounts

| Property / Method | Scenario | Returns |
| ----------------- | --------------------------------- | -------------- |
| List | stores a list of active discounts | List<Discount> |
| BgloDiscount() | | Discount |
| BglpDiscount() | | Discount |
| BgleDiscount() | | Discount |
| CofbDiscount() | | Discount |

### Prices

| Property | scenario | returns |
| ------------- | ------------------------------------ | --------------------------- |
| SkuToPriceMap | Dictionary mapping SKU to base price | Dictionary<string, decimal> |

### Products

| Method | Returns |
| ------ | ------- |
| BGLO() | Bagel |
| BGLP() | Bagel |
| BGLE() | Bagel |
| BGLS() | Bagel |
| COFB() | Coffee |
| COFW() | Coffee |
| COFC() | Coffee |
| COFL() | Coffee |
| FILB() | Filling |
| FILE() | Filling |
| FILC() | Filling |
| FILX() | Filling |
| FILS() | Filling |
| FILH() | Filling |
183 changes: 183 additions & 0 deletions exercise.main/Basket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using exercise.main.Collections;
using exercise.main.Exceptions;
using exercise.main.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;

namespace exercise.main
{
public class Basket
{
private int _capacity;
private List<IDiscountable> _products = new();


public List<IDiscountable> Products { get { return _products; } }
public int Capacity { get { return _capacity; } }

public Basket(int capacity)
{
_capacity = capacity;
}

public void Add(IDiscountable product)
{
if (product == null)
throw new ArgumentNullException();

if (_products.Count == _capacity)
throw new BasketFullException();

_products.Add(product);

CalculateDiscounts();
}

public void Remove(string SKU)
{
if (_products.Count == 0)
throw new ItemNotInBasketException();

if (_products.All(p => p.SKU != SKU))
throw new ItemNotInBasketException();

var itemToRemove = _products.Where(p => p.SKU == SKU).FirstOrDefault();
_products.Remove(itemToRemove);

CalculateDiscounts();
}

public void ChangeCapacity(int newCapacity)
{
_capacity = newCapacity;
}

public Decimal GetTotalCost()
{
return _products.Sum(p => p.GetFinalPrice());
}

private void CalculateDiscounts()
{
ResetDiscountedPrices();

var discounts = new Stack<Discount>(Discounts.List.OrderBy(p => p.SavedAmount));

var undiscountedItems = _products.Select(p => p).ToList();
var discountedItems = new List<IDiscountable>();

while (discounts.Count > 0)
{
var discount = discounts.Peek();

bool missingRequirements = false;
foreach (var kvp in discount.ItemRequirementAmountDict)
{
var count = undiscountedItems.Count(k => k.SKU == kvp.Key);
if (count < kvp.Value)
{
discounts.Pop();
missingRequirements = true;
break;
}
}

if (missingRequirements)
continue;

// apply discounted prices if requirement passed
// move discounted items to new list

// todo refactor use 1 list with DiscountState enum instead of undiscounted and discounted lists?

foreach (var kvp in discount.ItemRequirementAmountDict)
{
var newPrice = discount.ItemDiscountedPriceDict.GetValueOrDefault(kvp.Key);
var discounted = undiscountedItems.Where(p => p.SKU == kvp.Key).Take(kvp.Value).ToList();
discounted.ForEach(p => p.SetDiscountPrice(newPrice));
discounted.ForEach(p => p.IsDiscounted = true);
discountedItems.AddRange(discounted);

var itemsToRemove = undiscountedItems.Where(p => p.SKU == kvp.Key).Take(kvp.Value).ToList();
itemsToRemove.ForEach(i => undiscountedItems.Remove(i));
}
}

List<IDiscountable> newProducts = new();
newProducts.AddRange(undiscountedItems);
newProducts.AddRange(discountedItems);
_products = newProducts;
}

private void ResetDiscountedPrices()
{
_products.ForEach(p => p.IsDiscounted = false);
}

public void PrintReceipt(IReceiptPrinter printer)
{
var groupedByItem = _products.GroupBy(p => p.SKU);

string combinedItemsString = "";

foreach (var group in groupedByItem)
{
var name = group.FirstOrDefault().Name;
var price = group.Sum(p => p.GetFinalPrice());
var itemString = $"{name}" + " " + $"{group.Count()}" + " " + $"{price.ToString("F2")}" + "\n";

combinedItemsString += itemString;

if (group.Any(p => p.IsDiscounted))
{
var savedAmount = group.Sum(p => p.GetSavedAmount());
var savedAmountString = $" (-{savedAmount.ToString("F2")})\n";
combinedItemsString += savedAmountString;
}


}

var combinedReceiptString = "";
combinedReceiptString += GetReceiptStartString();
combinedReceiptString += combinedItemsString;
combinedReceiptString += GetReceiptEndString();

printer.Print(combinedReceiptString);
}

private string GetReceiptStartString()
{
var startString = "";
startString += " ~~~ Bob's Bagels ~~~\n";
startString += "\n";
startString += $" {DateTime.Now}\n";
startString += "\n";
startString += "----------------------------\n";
startString += "\n";


return startString;
}

private string GetReceiptEndString()
{
var totalPrice = GetTotalCost();

var endString = "";
endString += "\n";
endString += "----------------------------\n";
endString += $"Total {totalPrice.ToString("F2")}\n";
endString += "\n";
endString += " Thank you\n";
endString += " For your order!\n";


return endString;
}
}
}
77 changes: 77 additions & 0 deletions exercise.main/Collections/Discounts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace exercise.main.Collections
{
public static class Discounts
{
public static List<Discount> List = new List<Discount> {
{BgloDiscount()},
{BglpDiscount()},
{BgleDiscount()},
{CofbDiscount()},
};


public static Discount BgloDiscount()
{
return new Discount(
new Dictionary<string, int>
{
{"BGLO", 6}
},
new Dictionary<string, decimal>
{
{"BGLO", 0.415m}
}
);
}

public static Discount BglpDiscount()
{
return new Discount(
new Dictionary<string, int>
{
{"BGLP", 12}
},
new Dictionary<string, decimal>
{
{"BGLP", 0.3325m}
}
);
}

public static Discount BgleDiscount()
{
return new Discount(
new Dictionary<string, int>
{
{"BGLE", 6}
},
new Dictionary<string, decimal>
{
{"BGLE", 0.415m}
}
);
}

public static Discount CofbDiscount()
{
return new Discount(
new Dictionary<string, int>
{
{"COFB", 1},
{"BGLP", 1}
},
new Dictionary<string, decimal>
{
{"COFB", 0.99m},
{"BGLP", 0.26m}
}
);
}
}
}
Loading