- 
      
- 
        Save pauldambra/05577be969f351c39c9c to your computer and use it in GitHub Desktop. 
| defmodule Checkout do | |
| defstruct [ | |
| basket: %{A: 0, B: 0, C: 0, D: 0} | |
| ] | |
| def scan(checkout, code) do | |
| new_value = checkout.basket[code] + 1 | |
| %{checkout | basket: Map.put(checkout.basket, code, new_value)} | |
| end | |
| def total(checkout) do | |
| (for item <- checkout.basket, | |
| do: get_price item) | |
| |> Enum.sum | |
| end | |
| defp get_price({:A, amount}), do: get_special_price(%{amount: 3, price: 130}, 50, amount, 0) | |
| defp get_price({:B, amount}), do: get_special_price(%{amount: 2, price: 45}, 30, amount, 0) | |
| defp get_price({:C, amount}), do: get_price(20, amount) | |
| defp get_price({:D, amount}), do: get_price(15, amount) | |
| defp get_price(cost, amount) do | |
| cost * amount | |
| end | |
| defp get_special_price(offer, normal_cost, amount, current_total) do | |
| if amount >= offer.amount do | |
| get_special_price(offer, normal_cost, amount - offer.amount, current_total + offer.price) | |
| else # amount is now either less than the offer anmount or 0 | |
| current_total + get_price(normal_cost, amount) | |
| end | |
| end | |
| end | |
| defmodule CheckoutTest do | |
| use ExUnit.Case | |
| doctest Checkout | |
| # Item Unit Special | |
| # Price Price | |
| # -------------------------- | |
| # A 50 3 for 130 | |
| # B 30 2 for 45 | |
| # C 20 | |
| # D 15 | |
| setup do | |
| scans = [ | |
| %{items: [:A], total: 50}, | |
| %{items: [:B], total: 30}, | |
| %{items: [:C], total: 20}, | |
| %{items: [:D], total: 15}, | |
| %{items: [:A, :A], total: 100}, | |
| %{items: [:B, :B], total: 45}, | |
| %{items: [:A, :B], total: 80}, | |
| %{items: [:A, :A, :A], total: 130}, | |
| %{items: [:A, :A, :A, :A, :A], total: 230}, | |
| %{items: [:A, :A, :A, :B, :B], total: 175}, | |
| ] | |
| {:ok, data: scans} | |
| end | |
| test "scanning an item adds it to the basket" do | |
| checkout = Checkout.scan %Checkout{}, :A | |
| assert checkout.basket == %{A: 1, B: 0, C: 0, D: 0} | |
| end | |
| test "totalling a checkout can give a result" do | |
| checkout = Checkout.scan %Checkout{}, :A | |
| assert Checkout.total(checkout) == 50 | |
| end | |
| test "scanning a basket gives expected total cost", %{data: scans} do | |
| Enum.each(scans, | |
| fn(scan) -> | |
| total = Enum.reduce( | |
| scan.items, | |
| %Checkout{}, | |
| fn(item, chkot) -> Checkout.scan(chkot, item) end | |
| ) | |
| |> Checkout.total | |
| assert total == scan.total | |
| end | |
| ) | |
| end | |
| end | 
This is what I came up with:
defmodule Checkout do
  def scan(item) when is_map(item), do: scan(item, [])
  #convenience for passing in multiple items
  def scan(items) when is_list(items), do: Enum.reduce(items, [], &(scan(&1, &2)))
  def scan(item, order) when is_list(order), do: [item | order]
  def total(cart) do
    subtotal(cart) - discount(cart)
  end
  def discount(cart) do
    #calculate the discount
    calculate_discount(cart, :A, 3, 20.00) + calculate_discount(cart, :B, 2, 15.00)
  end
  defp calculate_discount(cart, identifier, discount_quantity, discount_value) do
    items = Enum.filter(cart, fn(item) ->
      item.sku == identifier
    end)
    total_items_count = Enum.reduce(items, 0, fn(item, acc) -> item.quantity + acc end)
    Float.floor(total_items_count / discount_quantity, 0) * discount_value
  end
  def subtotal(cart) do
    Enum.reduce cart, 0, fn(item, running_total) ->
      (item.price * item.quantity) + running_total
    end
  end
  defp decide_discount(item) do
    criteria = {item.sku, item.quantity}
    discount = case criteria do
      {:A, 3} -> 20.00
      {:B, 2} -> 15.00
      criteria -> 0 #default
    end
    Map.put_new(item, :discount, discount)
  end
end
defmodule CheckoutTest do
  use ExUnit.Case
  setup do
    items = [
      %{sku: :A, price: 50.00, quantity: 3},
      %{sku: :A, price: 50.00, quantity: 2},
      %{sku: :A, price: 50.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :C, price: 20.00, quantity: 1},
    ]
    {:ok, cart: items}
  end
  test "The cart is subtotaled without discount", %{cart: items} do
    total = Checkout.scan(items) |> Checkout.subtotal
    assert total == 410.00
  end
  test "The cart is totaled with discount", %{cart: items} do
    total = Checkout.scan(items) |> Checkout.total
    assert total == 355.00
  end
endMain problem with it is that the rules are hard-coded inside the discount function
Thinking about it, we should be able to pass in a list of rules to the discount function then just loop over them calling calculate_discount. Maybe.
Nice refactors! Yeah Tom I think having a discount callback with a set of rules would be interesting :).
Ok, came up with:
defmodule Checkout do
  def scan(item) when is_map(item), do: scan(item, [])
  #convenience for passing in multiple items
  def scan(items) when is_list(items), do: Enum.reduce(items, [], &(scan(&1, &2)))
  def scan(item, order) when is_list(order), do: [item | order]
  def total(cart, offers) do
    subtotal(cart) - discount(cart, offers)
  end
  def discount(cart, offers) do
    Enum.reduce(offers, 0, fn(offer, total) -> calculate_discount(cart, offer) + total end)
  end
  defp calculate_discount(cart, offer) do
    cart
      |> filter_by(offer.sku)
      |> count
      |> calculate_discount(offer.discount_qualification_quantity, offer.discount)
  end
  defp filter_by(cart, sku) do
    Enum.filter(cart, &(&1.sku == sku))
  end
  defp count(items) do
    Enum.reduce(items, 0, fn(item, total) -> item.quantity + total end)
  end
  defp calculate_discount(items_count, discount_qualification_quantity, discount_value) do
    Float.floor(items_count / discount_qualification_quantity, 0) * discount_value
  end
  def subtotal(cart) do
    Enum.reduce cart, 0, fn(item, running_total) ->
      (item.price * item.quantity) + running_total
    end
  end
end
defmodule CheckoutTest do
  use ExUnit.Case
  setup do
    items = [
      %{sku: :A, price: 50.00, quantity: 3},
      %{sku: :A, price: 50.00, quantity: 2},
      %{sku: :A, price: 50.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :C, price: 20.00, quantity: 1},
    ]
    {:ok, cart: items}
  end
  test "The cart is subtotalled without discount", %{cart: items} do
    total = Checkout.scan(items) |> Checkout.subtotal
    assert total == 410.00
  end
  test "The cart is totalled with discounts based on default offers", %{cart: items} do
    offers = [
      %{sku: :A, discount_qualification_quantity: 3, discount: 20},
      %{sku: :B, discount_qualification_quantity: 2, discount: 15}
    ]
    total = Checkout.scan(items) |> Checkout.total(offers)
    assert total == 355.00
  end
  test "The cart is totalled with discounts based on different offers", %{cart: items} do
    offers = [
      %{sku: :A, discount_qualification_quantity: 3, discount: 25},
      %{sku: :B, discount_qualification_quantity: 2, discount: 20}
    ]
    total = Checkout.scan(items) |> Checkout.total(offers)
    assert total == 340.00
  end
endAny suggestions welcome - this is my first attempt at writing any elixir, along with the Red:4 course! It's great fun so far!
Think the filter_by and count functions should possibly be one thing as what I want to know is how many of a certain sku are in the cart, just wasn't sure on the best way to write it. Could just stick it in one function, but wondered if Enum has anything build in that might do this
I'm not sure how to do it yet, but this is what I'm aiming for:
where the discount function calculates the discount on the items in the cart. This would mean changing the scan function to not calculate the discount: