## Ruby Invoice

September 2017 · 10 minute read

I have recently challenged my skills building a small program to process an order and generate an itemized invoice which calculates the optimum pack sizes to make up a given quantity for the best price.

The completed project can be viewed at https://github.com/SelenaSmall/ruby_invoice

## Project Description

A fresh food supplier sells product items to customers in packs. The bigger the pack, the cheaper the cost per item.

**The supplier currently sells the following products**

```
Product Packs
----------------------------------
Watermelons 3 pack @ $6.99
5 pack @ $8.99
Pineapples 2 pack @ $9.95
5 pack @ $16.95
8 pack @ $24.95
Rockmelons 3 pack @ $5.95
5 pack @ $9.95
9 pack @ $16.99
```

**Your task is to build a system that can take a customer order…**

For example, something like:

10 Watermelons 14 Pineapples 13 Rockmelons

**And generate an invoice for the order…**

For example, something like:

```
10 Watermelons $17.98
- 2 x 5 pack @ $8.99
14 Pineapples $54.80
- 1 x 8 pack @ $24.95
- 3 x 2 pack @ $9.95
13 Rockmelons $25.85
- 2 x 5 pack @ $9.95
- 1 x 3 pack @ $5.95
-----------------------------
TOTAL $98.63
```

**Note**that the system has determined the optimal packs to fill the order. You can assume that bigger packs will always have a cheaper cost per unit price.

## Planing

The first step in any new project, of course is consider your requirements and make a plan. It’s likely the plan will change later, but it’s a start point.

**Why did I choose ruby?**

I’m most familiar to me so I will get the product out in a reasonable timeframe.

Just worked out a great way to practice TDD for ruby apps using Travis-CI.

I can see this product being object oriented therefore ruby seems like a reasonable fit.

**Objects**

I’m probably going to split the compnents up into individual objects as follows:

item(item_name, pack)

pack(qty, price)

- child of item (subClass)

order(items = [], basket)

basket(current_order=nil)

order_line(qty, item)

- calculate optimal packs required to make up the qty
- calculate total price of packs per product

invoice(order)

`order.items.each do | item | puts item.get_receipt_line end`

**Input**

This is my planned perception of how the program will work:

```
Would you like to LIST available products, SHOP, VIEW basket, EXIT without placing an order?
```

$_ LIST

```
list_products & packs
```

$_ SHOP

```
place your order:
```

$_ 10 watermelons

$_ VIEW

```
10 Watermelons $17.98
- 2 x 5 pack @ $8.99
——————————————————————————————
TOTAL $17.98
Would you like to complete your order and checkout now?
```

$_ yes

```
> Your order has been placed, thank you. Goodbye!
```

$_ EXIT

**OR**

```
> Your order has not been placed, are you sure you want to leave?
```

$_ yes

```
> Goodbye!
```

**Assumptions**

- If someone orders a quantity of product which does not equal a quantity made up of packs, they will be charged for the the nearest quantity above what they have ordered.

ie 11 Watermelons = 2x 5 packs + 1x 3pack

## Build Process

**Initialize_repo**

Set up Travis-CI and blank app to get started

https://github.com/SelenaSmall/ruby_invoice/commit/d16526d753b67aef99035c957471433a09548fa6

**1. Create items and packs**

Item class

Pack class

Watermelon class

I think for scalability, Watermelon should be a subclass of Item - I’m not quite sure yet how this is going to work. For now, I will just focus on getting the base of the app working.

Each sub-item will have a range of Packs available with pre-determined item qty’s and prices.

https://github.com/SelenaSmall/ruby_invoice/commit/f2812ece9a4d347417e0651a794e5d972708801d

**2. Handle user inputs**

- Handle_input class

I put this in next because it makes me feel better to know what the end result will connect to. At this stage, the only thing being checked is that a command is valid.

https://github.com/SelenaSmall/ruby_invoice/commit/828dc25d386207223dc6f67111d8f3427766c5a3

**3. Define orders**

- Basket class

Need an empty basket to put the orders into

- Order class

To put the items into the basket

https://github.com/SelenaSmall/ruby_invoice/commit/925b83c8fe0578ec24bc40e2e4e179a30fe842c2

**4. Define each line in the order**

- Order_line class

The quantity and name of the item requested to be ordered by customer input

https://github.com/SelenaSmall/ruby_invoice/commit/ffba1411bafd814d9057501ade50c5c722d0a4c7

**5. Determine optimal packs for each order line**

Given what I have so far, there is enough to make the SHOP and LIST actions work with my HandleInput class.

**SHOP** should allow user to input an order_line in the format “3 watermelon”

- OrderLine needs to determine the optimal # of packs to fill the order:

```
packs = []
left_over_qty = order_qty
pack.each (starting from largest value) do |p|
left_over_qty / p
for each whole result, packs << p
left_over_qty = remainder
next
end
```

https://github.com/SelenaSmall/ruby_invoice/commit/424c2d597de3150800030276ab9faff66701f70d https://github.com/SelenaSmall/ruby_invoice/commit/120a507f8be82fdb81753abc9a6b00aa838535f3

**6. Add orderline packs to Order**

- OrderLine needs to be added to the overall Order

Both Basket and Order probably aren’t required at this point

I’ll start by initialising the Order object in app entry and update it with every additional item added when ‘shopping’

https://github.com/SelenaSmall/ruby_invoice/commit/ddd08f537eff38792eeb2b5e222d9f6daba064cb

**7. Review and refactor**

Got a bunch of code working and doing it’s job. Time for a tidy up!

- Refactor the code and revise tests

Although there is no duplicate code, there is certainly room for improvements and methods can be broken down into smaller easier-to-work-with components

https://github.com/SelenaSmall/ruby_invoice/commit/e7df573d38fd1178cc567e3d0b70d65406d17b5a

**8. Define invoice**

- Invoice class

**VIEW** should output the itemised invoice of the full order

https://github.com/SelenaSmall/ruby_invoice/commit/f232c0bf3bcbbc054b6c69d2fe1b01bdd50a0d18

**9. Define additional products**

Create additional fruit items

Reorganise code and review tests again.

Refactor shop_menu into it’s own method

https://github.com/SelenaSmall/ruby_invoice/commit/56b658717a5ee41287142733e5cd7e73c1f24e81

**10. Define LIST action**

**LIST** should output a list of available items with their pack size and quantity. I probably should have listed the options first, but I was on a roll with SHOP and now that I’ve gotten this far, it seems less relevant. Still it will tie the app nicely to be able to view all available products and pack sizes.

**11. Break down HandleInput methods**

The handle_input class is getting too heavy, especially considering the size of the app. I’ve just taken this opportunity to also move the shop methods out into their own Shop object

https://github.com/SelenaSmall/ruby_invoice/commit/4b96f158a107d02dcc69d58fff9ce34d742f3f35

**12. Review OrderLine**

I’m still pretty unhappy with the OrderLine methods. They’re chunky and painful to look at, which leads me to believe there must be a better way.

It also looks like I’ve made a mistake in selecting optimal pack sizes. I focussed too much on trying to make up qty’s which would not be an exact product of pack sizes available and in the process overlooked searching for an exact match first.

What I actually should have done was check if an exact match is possible first.

```
if order_qty === any sum of pack sizes
make up order with those
(using lowest price combo)
else
go get whole/left_over_pieces to make up closest qty packs
OR
if qty does not add up, ignore it.
end
```

This is actually trickier than I initially thought, but the best way to approach it is to break it right down and play around with a simple array in a seperate window. Here’s the test I ended up writing to get the whole optimal selection part working.

```
class ArrayCheck
attr_reader :pack_qtys
def initialize
@pack_qtys = [[2, 6], [5, 9], [8, 16]]
end
def optimal(qty)
# Get Exact matches
exact_matches = []
pack_qtys.sort { |a, b| b <=> a }.each do |p, v|
next unless (qty / p) * p == qty
exact_matches << [qty / p, p, v]
end
# puts exact_matches
# Check for the optimim price
price_check = []
exact_matches.each do |x, y, z|
val = (x * z)
price_check << [x, y, z, val]
end
# puts price_check
numbers = price_check.select { |x| x[3] }.map
# Get Part matches
partial_matches = []
pack_qtys.sort { |a, b| b <=> a }.each do |p, v|
partial_matches << [qty / p, p, v]
end
# puts partial_matches
# Top up part matches
exact_partial = []
partial_matches.each do |x, y, z|
val = qty - (x * y)
pack_qtys.detect do |a|
if a.include?(val)
exact_partial << [x, y, z]
exact_partial << [val / a[0], a[0], a[1]]
end
end
end
# puts exact_partial
# Check for the optimim partial price
partial_price_check = []
exact_partial.each do |x, y, z|
val = (x * z)
partial_price_check << [x, y, z, val]
end
# puts partial_price_check
partial_numbers = partial_price_check.select { |x| x[3] }.map
partial_price_array = []
partial_numbers.each do |f|
partial_price_array << f
end
# puts partial_price_array
puts "Exact Match: #{exact_matches}"
# Array for bext price line: 1x 9pk @16 = $16
puts "Best Exact Price: #{numbers.min}"
puts "Partial Match: #{exact_partial}"
puts "Best Partial Price: #{partial_price_array}"
calculate_best(numbers.min, partial_price_array)
end
def calculate_best(exact, partial)
sub_total = []
partial.each do |_x, _y, _z, val|
sub_total << val
end
# Find total cost of sub_items
line_total = sub_total.inject(:+)
# Return array of the cheapest line
puts "\nThis is it #{partial}" if [exact[3], line_total].min == line_total
puts "\nThis is it #{exact}" if [exact[3], line_total].min == exact
end
end
array = ArrayCheck.new
array.optimal(14)
```

Transfer the test code into my project and wallah! The pack selections that are meant to be made on orders are now being made!!

It’s late now, so before I call it a day I’m just going to make sure my existing tests pass. Although this is now working, it’s certainly not finished. I’ll break down the code in cleaner methods and write the tests fro them tomorrow.

https://github.com/SelenaSmall/ruby_invoice/commit/ace50ed589c4e0a3bb1038e716633324d54e3b63

**13. Optimise OrderLine**

Same process. Now that the system is actually working the way it should, I can go through and do a clean up.

**a) Ensure all items are returned in the same format**

Optimal method responses as determined by calculate_best method - I want everything to be returned as an Enumerator.

**b) Split optimal sections out into individual methods**

This will thin down the optimal method and make it easier to see what’s going on. By adding doc blocks, I will also be able to see clearly any duplicate methods.

```
def exact_match(pack_qtys, exact_matches)
pack_qtys.sort { |a, b| b <=> a }.each do |p, v|
next unless (order_qty / p) * p == order_qty
exact_matches << [order_qty / p, p, v]
end
exact_matches
end
def price_check(exact_matches, price_check)
exact_matches.each do |x, y, z|
val = (x * z)
price_check << [x, y, z, val]
end
price_check
end
def partial_matches(pack_qtys, partial_matches)
pack_qtys.sort { |a, b| b <=> a }.each do |p, v|
partial_matches << [order_qty / p, p, v]
end
partial_matches
end
def exact_partial(pack_qtys, partial_matches, exact_partial)
partial_matches.each do |x, y, z|
val = order_qty - (x * y)
puts "VAL: #{val}"
pack_qtys.detect do |a|
# To be optimised: a.include?(val) does not cooperate with Money
if a[0] == val || a[0] * 3 == val
exact_partial << [x, y, z]
exact_partial << [val / a[0], a[0], a[1]]
end
end
end
exact_partial
end
def partial_price_check(exact_partial, partial_price_check)
exact_partial.each do |x, y, z|
val = (x * z)
partial_price_check << [x, y, z, val]
end
partial_price_check
end
def partial_price_array(partial_numbers, partial_price_array)
partial_numbers.each do |f|
partial_price_array << f
end
partial_price_array
end
```

**c) Review those methods**

Look to see where duplicate code can be cut out and find methods which are not required.

There are two identical methods - I only need this code once:

price_check

partial_price_check

Additionally, two methods are called currently to return one object. The difference is one extra param - these can be combined into one method.

partial_matches

exact_partial

There are now two identical lines in the optimal method which means they can be abstracted out into their own method.

exact_match_prices = exact_price_check.select { |x| x[3] }.map

partial_numbers = partial_price_check.select { |x| x[3] }.map

And they will become:

```
def match_prices(array)
array.select { |x| x[3] }.map
end
```

I’ve also got a completely unnecessary method, since I’ll be handling Enumerators instead of Arrays, I won’t need this:

- partial_price_array

**d) Clean up calculate_best method**

**e) Optimal method check**

- Return unless there is an optimal match for packs (ignore the order item).

https://github.com/SelenaSmall/ruby_invoice/commit/1d97d008f8fd21ba852eb89d12920803f6b80ea7 https://github.com/SelenaSmall/ruby_invoice/commit/b3c7b73cc5483b71b9247df037a4eacd0b433235

## Documentation & Final Review

Of course, no good code is complete without doc blocks and no project is complete without a README. This is also a good chance to review and add additional tests which might have been overlooked. - Finished up with tests passing a 99% code coverage.

https://github.com/SelenaSmall/ruby_invoice/commit/199b7b4cde020a4b89fc6762e052604e47008923