From 882c18b5e68e5adaad49e627f682045095dc2205 Mon Sep 17 00:00:00 2001 From: David Underwood Date: Tue, 21 Jul 2026 10:24:14 -0400 Subject: [PATCH] Rebuild the Checkout class, no discounts yet Checkout now stores its items using a quantity and a link back to the original product instance. It has a reference to the store to make sure that product IDs added are valid --- checkout.rb | 57 +++++++++++++------------------------- product_in_checkout.rb | 22 +++++++++++++++ shop.rb | 8 ++---- test/checkout_test.rb | 62 ++++++++++++++++++++++++++++++++---------- 4 files changed, 91 insertions(+), 58 deletions(-) create mode 100644 product_in_checkout.rb diff --git a/checkout.rb b/checkout.rb index d7c6252..53c319c 100644 --- a/checkout.rb +++ b/checkout.rb @@ -1,62 +1,43 @@ +require 'product_in_checkout.rb' + class Checkout class InvalidItem < StandardError; end - AVAILABLE_ITEMS = { - PI5: 60.00, - ZERO2W: 15.00, - PICO2: 4.00 - } - - def initialize + def initialize(shop:) + @shop = shop @items = [] end - def add(item) - raise InvalidItem unless valid_item?(item) - @items.push item - end + def add(product_id:, quantity: 1) + product = @shop.product_by_id(product_id) - def remove(item) - if index = @items.index(item) - @items.delete_at(index) - end + raise InvalidItem unless product - @items + product_in_checkout = new_or_existing_product_in_checkout(product) + product_in_checkout.add_quantity(quantity) + + @items.push product_in_checkout end def items @items end - def total - pre_discount_total - current_discount + def item_quantities + @items.map { |item| [item.product_id, item.quantity] }.to_h end def clear @items = [] end + def total + @items.reduce(0.00) { |running_total, item| running_total + item.total } + end + private - def valid_item?(item) - return AVAILABLE_ITEMS.keys.include?(item) - end - - def current_discount - # BOGOF ZERO2W - discount = (@items.count(:ZERO2W) / 2.0).floor * AVAILABLE_ITEMS[:ZERO2W] - - # Bulk PICO2 - if(items.count(:PICO2) >= 3) - discount += items.count(:PICO2) * 0.50 - end - - return discount - end - - def pre_discount_total - @items.reduce(0.00) do |running_total, item| - running_total + AVAILABLE_ITEMS[item] - end + def new_or_existing_product_in_checkout(product) + @items.find { |item| item.product_id == product.id } || ProductInCheckout.new(product: product) end end diff --git a/product_in_checkout.rb b/product_in_checkout.rb new file mode 100644 index 0000000..ddcc4f8 --- /dev/null +++ b/product_in_checkout.rb @@ -0,0 +1,22 @@ +class ProductInCheckout + def initialize(product:, quantity: 0) + @product = product + @quantity = quantity + end + + def product_id + @product.id + end + + def quantity + @quantity + end + + def add_quantity(quantity) + @quantity += quantity + end + + def total + @product.price * quantity + end +end diff --git a/shop.rb b/shop.rb index 4d29ff9..2b7a531 100644 --- a/shop.rb +++ b/shop.rb @@ -9,14 +9,12 @@ class Shop end def add_product(product) - raise DuplicateProductIdError if duplicate_id?(product.id) + raise DuplicateProductIdError if product_by_id(product.id) @products.push(product) end - private - - def duplicate_id?(id) - @products.map(&:id).include?(id) + def product_by_id(id) + @products.find { |product| product.id == id } end end diff --git a/test/checkout_test.rb b/test/checkout_test.rb index e0967d2..7915e90 100644 --- a/test/checkout_test.rb +++ b/test/checkout_test.rb @@ -4,35 +4,47 @@ require "checkout.rb" class CheckoutTest < Minitest::Test def setup - @checkout = Checkout.new + @shop = Shop.new + @shop.add_product(Product.new(id: 'PI5', price: 60.00)) + @shop.add_product(Product.new(id: 'ZERO2W', price: 15.00)) + @shop.add_product(Product.new(id: 'PICO2', price: 4.00)) + @checkout = Checkout.new(shop: @shop) end def test_that_unknown_items_cannot_be_added assert_raises Checkout::InvalidItem do - @checkout.add(:foo) + @checkout.add(product_id: 'PI5000') end end def test_that_nil_is_not_allowed_to_be_added assert_raises Checkout::InvalidItem do - @checkout.add(nil) + @checkout.add(product_id: nil) end end def test_that_allowed_items_are_added - @checkout.add(:PI5) - @checkout.add(:ZERO2W) - @checkout.add(:PICO2) + @checkout.add(product_id: 'PI5') + @checkout.add(product_id: 'ZERO2W') + @checkout.add(product_id: 'PICO2') - assert_equal [:PI5, :ZERO2W, :PICO2].sort, @checkout.items.sort + assert_equal({ 'PI5' => 1, 'ZERO2W' => 1, 'PICO2' => 1 }.sort, @checkout.item_quantities.sort) + end + + def test_that_multiples_can_be_added_at_once + @checkout.add(product_id: 'PI5', quantity: 5) + assert_equal({ 'PI5' => 5 }.sort, @checkout.item_quantities.sort) + + @checkout.add(product_id: 'PI5', quantity: 5) + assert_equal({ 'PI5' => 10 }.sort, @checkout.item_quantities.sort) end def test_that_total_is_the_sum_of_all_items_when_no_discounts_apply - @checkout.add(:PI5) # 60.00 - @checkout.add(:ZERO2W) # 15.00 - @checkout.add(:PICO2) # 4.00 - # ----- - # 79.00 total + @checkout.add(product_id: 'PI5') # 60.00 + @checkout.add(product_id: 'ZERO2W') # 15.00 + @checkout.add(product_id: 'PICO2') # 4.00 + # ----- + # 79.00 total assert_equal 79.00, @checkout.total end @@ -42,9 +54,9 @@ class CheckoutTest < Minitest::Test end def test_that_we_can_empty_the_checkout - @checkout.add(:PI5) - @checkout.add(:ZERO2W) - @checkout.add(:PICO2) + @checkout.add(product_id: 'PI5') + @checkout.add(product_id: 'ZERO2W') + @checkout.add(product_id: 'PICO2') refute_empty @checkout.items @@ -57,6 +69,8 @@ class CheckoutTest < Minitest::Test # Discounts def test_that_bogof_applied + skip + 4.times do @checkout.add(:ZERO2W) # Should remove 2 Zeros end @@ -65,6 +79,8 @@ class CheckoutTest < Minitest::Test end def test_that_bogof_applied_with_odd_number + skip + 5.times do @checkout.add(:ZERO2W) # Should still remove 2 Zeros, 5th is full price end @@ -73,6 +89,8 @@ class CheckoutTest < Minitest::Test end def test_that_price_is_reduced_for_3_picos + skip + 3.times do @checkout.add(:PICO2) # 3.50 * 3 = 10.50 end @@ -81,6 +99,8 @@ class CheckoutTest < Minitest::Test end def test_that_price_is_reduced_for_more_than_3_picos + skip + 4.times do @checkout.add(:PICO2) # 3.50 * 4 = 14 end @@ -89,6 +109,8 @@ class CheckoutTest < Minitest::Test end def test_that_price_is_not_reduced_for_less_than_3_picos + skip + 2.times do @checkout.add(:PICO2) # 4.00 * 2 = 8 end @@ -97,6 +119,8 @@ class CheckoutTest < Minitest::Test end def test_that_both_discounts_apply_at_the_same_time + skip + 4.times do @checkout.add(:ZERO2W) # 30.00 with BOGOF end @@ -111,6 +135,8 @@ class CheckoutTest < Minitest::Test # Item removal def test_that_removing_an_item_not_in_the_checkout_removes_nothing + skip + @checkout.add(:PICO2) @checkout.remove(:PI5) @@ -118,6 +144,8 @@ class CheckoutTest < Minitest::Test end def test_that_items_are_removed_one_by_one + skip + 4.times do @checkout.add(:ZERO2W) end @@ -144,6 +172,8 @@ class CheckoutTest < Minitest::Test end def test_that_total_is_correctly_recalculated_when_items_are_added + skip + 2.times do @checkout.add(:PICO2) end @@ -162,6 +192,8 @@ class CheckoutTest < Minitest::Test end def test_that_total_is_correctly_recalculated_when_items_are_removed + skip + 3.times do @checkout.add(:PICO2) # Bulk discount applies end