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