From 4c1693f74c25ee60e23c4a3545f9643aa7027986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 15:44:31 +0200 Subject: [PATCH 01/27] let time promotion last longer during the test it's hard to debug if promotion ends in a second --- ecommerce/pricing/test/time_promotion_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce/pricing/test/time_promotion_test.rb b/ecommerce/pricing/test/time_promotion_test.rb index a3ccd999..f006df52 100644 --- a/ecommerce/pricing/test/time_promotion_test.rb +++ b/ecommerce/pricing/test/time_promotion_test.rb @@ -76,7 +76,7 @@ def test_calculates_sub_amounts_with_combined_discounts time_promotion_id: SecureRandom.uuid, discount: 50, start_time: Time.current - 1, - end_time: Time.current + 1, + end_time: Time.current + 100, label: "Last Minute" } From 69e7ad4bb211005b9bdcc39538f48d735b9775eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 15:45:44 +0200 Subject: [PATCH 02/27] publish more pricing info in ItemAdded/ItemRemoved event payloads * add base_price * add base_total_value and total_value. This is the preliminary step to remove Totals Calculation events in the future --- ecommerce/pricing/lib/pricing/events.rb | 6 +++ ecommerce/pricing/lib/pricing/offer.rb | 49 +++++++++++++------ .../pricing/test/apply_time_promotion_test.rb | 3 ++ 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index d5d52a75..5b7928bd 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -41,13 +41,19 @@ class PercentageDiscountSet < Infra::Event class PriceItemAdded < Infra::Event attribute :order_id, Infra::Types::UUID attribute :product_id, Infra::Types::UUID + attribute :base_price, Infra::Types::Price attribute :price, Infra::Types::Price + attribute :base_total_value, Infra::Types::Price + attribute :total_value, Infra::Types::Price end class PriceItemRemoved < Infra::Event attribute :order_id, Infra::Types::UUID attribute :product_id, Infra::Types::UUID + attribute :base_price, Infra::Types::Price attribute :price, Infra::Types::Price + attribute :base_total_value, Infra::Types::Price + attribute :total_value, Infra::Types::Price end class PercentageDiscountRemoved < Infra::Event diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index d601dd82..c5baf158 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -12,23 +12,30 @@ def initialize(id) @state = :draft end - def add_item(product_id, price) + def add_item(product_id, base_price) + price = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(base_price) apply PriceItemAdded.new( data: { order_id: @id, product_id: product_id, + base_price: base_price, price: price, + base_total_value: @list.base_sum + base_price, + total_value: @list.actual_sum + price } ) end def remove_item(product_id) - price = @list.lowest_price(product_id) + item = @list.lowest_price_item(product_id) apply PriceItemRemoved.new( data: { order_id: @id, product_id: product_id, - price: price + base_price: item.base_price, + price: item.price, + base_total_value: @list.base_sum - item.base_price, + total_value: @list.actual_sum - item.price } ) end @@ -87,7 +94,7 @@ def remove_free_product(order_id, product_id) def calculate_total_value total_value = @list.base_sum - discounted_value = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(total_value) + discounted_value = @list.actual_sum apply( OrderTotalValueCalculated.new( @@ -110,8 +117,8 @@ def calculate_sub_amounts order_id: @id, product_id: product_id, quantity: h.fetch(:quantity), - amount: h.fetch(:amount), - discounted_amount: @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(h.fetch(:amount)) + amount: h.fetch(:base_amount), + discounted_amount: h.fetch(:amount), } ) ) @@ -156,7 +163,7 @@ def expire private on PriceItemAdded do |event| - @list.add_item(event.data.fetch(:product_id), event.data.fetch(:price)) + @list.add_item(event.data.fetch(:product_id), event.data.fetch(:base_price), event.data.fetch(:price)) end on PriceItemRemoved do |event| @@ -171,15 +178,18 @@ def expire on PercentageDiscountSet do |event| @discounts << Discounts::PercentageDiscount.new(event.data.fetch(:type), event.data.fetch(:amount)) + @list.apply_discounts(@discounts) end on PercentageDiscountChanged do |event| @discounts.delete_if { |discount| discount.type == event.data.fetch(:type) } @discounts << Discounts::PercentageDiscount.new(event.data.fetch(:type), event.data.fetch(:amount)) + @list.apply_discounts(@discounts) end on PercentageDiscountRemoved do |event| @discounts.delete_if { |discount| discount.type == event.data.fetch(:type) } + @list.apply_discounts(@discounts) end on ProductMadeFreeForOrder do |event| @@ -210,16 +220,14 @@ def discount_exists?(type) end class List - Item = Data.define(:product_id, :quantity, :price, :catalog_price) do - def initialize(product_id:, quantity:, price:, catalog_price: price) = super - end + Item = Data.define(:product_id, :quantity, :base_price, :price) def initialize @items = [] end - def add_item(product_id, price) - @items << Item.new(product_id:, price:, quantity: 1) + def add_item(product_id, base_price, price) + @items << Item.new(product_id:, base_price:, price:, quantity: 1) end def remove_item(product_id, price) @@ -227,17 +235,29 @@ def remove_item(product_id, price) @items.delete_at(index_of_item_to_remove) end + def apply_discounts(discounts) + @items = @items.map do |item| + price = discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(item.base_price) + item.with(price:) + end + end + def contains_free_products? @items.any? { |item| item.price == 0 } end def base_sum + @items.sum(&:base_price) + end + + def actual_sum @items.sum(&:price) end def sub_amounts_total @items.each_with_object({}) do |item, memo| - memo[item.product_id] ||= { amount: 0, quantity: 0 } + memo[item.product_id] ||= { base_amount: 0, amount: 0, quantity: 0 } + memo[item.product_id][:base_amount] += item.base_price * item.quantity memo[item.product_id][:amount] += item.price * item.quantity memo[item.product_id][:quantity] += item.quantity end @@ -256,12 +276,11 @@ def restore_nonfree(product_id) @items << old_item.with(price: old_item.catalog_price) end - def lowest_price(product_id) + def lowest_price_item(product_id) @items .select { |item| item.product_id == product_id } .sort_by(&:price) .first - &.price end def quantities diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb index d8e2678d..87a9ff64 100644 --- a/ecommerce/pricing/test/apply_time_promotion_test.rb +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -61,7 +61,10 @@ def item_added_to_basket_event(order_id, product_id) data: { product_id: product_id, order_id: order_id, + base_price: 1111, price: 1111, + base_total_value: 1111, + total_value: 1111 } ) end From 14bb5b88e2210a830bd34da96febc6ae6f174394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 15:51:06 +0200 Subject: [PATCH 03/27] fix free products test I believe, they was broken before. In my opinion, total_value should take into account ALL products base_price, event these made free --- ecommerce/pricing/lib/pricing/offer.rb | 2 +- ecommerce/pricing/test/free_products_test.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index c5baf158..daf9afd7 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -273,7 +273,7 @@ def restore_nonfree(product_id) idx = @items.index { |item| item.product_id == product_id && item.price == 0 } return unless idx old_item = @items.delete_at(idx) - @items << old_item.with(price: old_item.catalog_price) + @items << old_item.with(price: old_item.base_price) end def lowest_price_item(product_id) diff --git a/ecommerce/pricing/test/free_products_test.rb b/ecommerce/pricing/test/free_products_test.rb index a4f0164d..a2423401 100644 --- a/ecommerce/pricing/test/free_products_test.rb +++ b/ecommerce/pricing/test/free_products_test.rb @@ -25,7 +25,7 @@ def test_making_product_free_possible_when_order_is_eligible data: { order_id: order_id, discounted_amount: 60, - total_amount: 60 + total_amount: 80 } ) ) do @@ -58,7 +58,7 @@ def test_making_only_the_cheapest_product_free data: { order_id: order_id, discounted_amount: 60, - total_amount: 60 + total_amount: 70 } ), ) do @@ -125,7 +125,7 @@ def test_making_product_free_possible_after_previous_free_product_was_removed data: { order_id: order_id, discounted_amount: 60, - total_amount: 60 + total_amount: 80 } ) ) do From a35c145bab1f1f3e2d293af05852b6707de8db56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 16:09:56 +0200 Subject: [PATCH 04/27] add more Pricing::Offer tests to extend coverage and kill mutants --- ecommerce/pricing/test/simple_offer_test.rb | 112 ++++++++++++++++++-- 1 file changed, 105 insertions(+), 7 deletions(-) diff --git a/ecommerce/pricing/test/simple_offer_test.rb b/ecommerce/pricing/test/simple_offer_test.rb index a8dd80eb..72a7dac8 100644 --- a/ecommerce/pricing/test/simple_offer_test.rb +++ b/ecommerce/pricing/test/simple_offer_test.rb @@ -2,27 +2,125 @@ module Pricing class SimpleOfferTest < Test - cover "Pricing*" + cover "Pricing::Offer*" + + def test_adding + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 20, + total_value: 20, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 20, + total_amount: 20 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 1, + amount: 20, + discounted_amount: 20, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 40, + total_value: 40, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 40, + total_amount: 40 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 2, + amount: 40, + discounted_amount: 40, + } + ) + ) { add_item(order_id, product_id) } + + end def test_removing - product_1_id = SecureRandom.uuid - set_price(product_1_id, 20) + product_id = SecureRandom.uuid + set_price(product_id, 20) order_id = SecureRandom.uuid - add_item(order_id, product_1_id) + add_item(order_id, product_id) + add_item(order_id, product_id) stream = "Pricing::Offer$#{order_id}" + assert_events( stream, + PriceItemRemoved.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 20, + total_value: 20, + } + ), OrderTotalValueCalculated.new( data: { order_id: order_id, discounted_amount: 20, total_amount: 20 } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 1, + amount: 20, + discounted_amount: 20, + } ) - ) { calculate_total_value(order_id) } - remove_item(order_id, product_1_id) + ) { remove_item(order_id, product_id) } + assert_events( stream, + PriceItemRemoved.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 0, + total_value: 0, + } + ), OrderTotalValueCalculated.new( data: { order_id: order_id, @@ -30,7 +128,7 @@ def test_removing total_amount: 0 } ) - ) { calculate_total_value(order_id) } + ) { remove_item(order_id, product_id) } end end end From e08fcea20fcd55b2a10cf3c16f7418f36f686991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 16:35:53 +0200 Subject: [PATCH 05/27] kill mutants --- ecommerce/pricing/test/time_promotion_test.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ecommerce/pricing/test/time_promotion_test.rb b/ecommerce/pricing/test/time_promotion_test.rb index f006df52..c07a7d6f 100644 --- a/ecommerce/pricing/test/time_promotion_test.rb +++ b/ecommerce/pricing/test/time_promotion_test.rb @@ -38,13 +38,12 @@ def create_time_promotion(**kwargs) end class DiscountWithTimePromotionTest < Test - cover "Pricing*" + cover "Pricing::Offer*" def test_calculates_total_value_with_time_promotion order_id = SecureRandom.uuid product_1_id = SecureRandom.uuid set_price(product_1_id, 20) - add_item(order_id, product_1_id) stream = stream_name(order_id) time_promotion_id = SecureRandom.uuid start_time = Time.current - 1 @@ -55,6 +54,16 @@ def test_calculates_total_value_with_time_promotion assert_events_contain( stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_1_id, + base_price: 20, + price: 10, + base_total_value: 20, + total_value: 10, + } + ), OrderTotalValueCalculated.new( data: { order_id: order_id, @@ -62,7 +71,7 @@ def test_calculates_total_value_with_time_promotion discounted_amount: 10 } ) - ) { calculate_total_value(order_id) } + ) { add_item(order_id, product_1_id) } end def test_calculates_sub_amounts_with_combined_discounts From 079ed33f2dc981bb70b29bbf4df89ec700c69842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 16:48:56 +0200 Subject: [PATCH 06/27] fix ThreeePlusOneFreeTests WARNING. Events generated by add_item/remove_item helpers are not fully proper. The totals in their payloads can be invalid. This process doesn't care about totals, so tests are green. Isn't it a smell? --- ecommerce/processes/test/three_plus_one_free_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ecommerce/processes/test/three_plus_one_free_test.rb b/ecommerce/processes/test/three_plus_one_free_test.rb index 87162594..e5172fe3 100644 --- a/ecommerce/processes/test/three_plus_one_free_test.rb +++ b/ecommerce/processes/test/three_plus_one_free_test.rb @@ -105,17 +105,17 @@ def set_price(product_id, amount) end def item_added_event(order_id, product_id, price, times: 1) - times.times.collect do + times.times.collect do |i| Pricing::PriceItemAdded.new( - data: { order_id:, product_id:, price: } + data: { order_id:, product_id:, price:, base_price: price, base_total_value: (i+1) * price, total_value: (i+1) * price } ) end end def item_removed_event(order_id, product_id, price, times: 1) - times.times.collect do + times.times.collect do |i| Pricing::PriceItemRemoved.new( - data: { order_id:, product_id:, price: } + data: { order_id:, product_id:, price:, base_price: price, base_total_value: (i+1)*price, total_value: (i+i)*price } ) end end From 6f43584b56000d77d19776780aac059ffc061c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Roma=C5=84czuk?= Date: Wed, 16 Apr 2025 17:06:52 +0200 Subject: [PATCH 07/27] fix tests in rails_application after adding new attributes to ItemAdded/ItemRemoved --- .../item_added_to_basket_test.rb | 15 +++++++++++ .../item_removed_from_basket_test.rb | 27 +++++++++++++++++++ .../test/client_orders/order_expired_test.rb | 5 +++- .../test/orders/broadcast_test.rb | 21 +++++++++++++++ .../test/orders/item_added_to_basket_test.rb | 15 +++++++++++ .../orders/item_removed_from_basket_test.rb | 27 +++++++++++++++++++ .../test/orders/order_expired_test.rb | 5 +++- .../test/orders/order_placed_test.rb | 5 +++- .../remove_time_promotion_discount_test.rb | 5 ++-- .../orders/update_order_total_value_test.rb | 3 ++- ...date_time_promotion_discount_value_test.rb | 2 +- 11 files changed, 123 insertions(+), 7 deletions(-) diff --git a/rails_application/test/client_orders/item_added_to_basket_test.rb b/rails_application/test/client_orders/item_added_to_basket_test.rb index d4968cd1..14102419 100644 --- a/rails_application/test/client_orders/item_added_to_basket_test.rb +++ b/rails_application/test/client_orders/item_added_to_basket_test.rb @@ -28,7 +28,10 @@ def test_add_new_item data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 49, + total_value: 49, } ) ) @@ -71,7 +74,10 @@ def test_add_the_same_item_2nd_time data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 49, + total_value: 49, } ) ) @@ -81,7 +87,10 @@ def test_add_the_same_item_2nd_time data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 98, + total_value: 98, } ) ) @@ -140,7 +149,10 @@ def test_add_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -150,7 +162,10 @@ def test_add_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) diff --git a/rails_application/test/client_orders/item_removed_from_basket_test.rb b/rails_application/test/client_orders/item_removed_from_basket_test.rb index 60e83faa..48aab416 100644 --- a/rails_application/test/client_orders/item_removed_from_basket_test.rb +++ b/rails_application/test/client_orders/item_removed_from_basket_test.rb @@ -30,7 +30,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -39,7 +42,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) @@ -48,7 +54,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -86,7 +95,10 @@ def test_remove_item_when_quantity_eq_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -95,7 +107,10 @@ def test_remove_item_when_quantity_eq_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 0, + total_value: 0, } ) ) @@ -145,7 +160,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -154,7 +172,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) @@ -163,7 +184,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 60, + total_value: 60, } ) ) @@ -172,7 +196,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) diff --git a/rails_application/test/client_orders/order_expired_test.rb b/rails_application/test/client_orders/order_expired_test.rb index f0b4c2bd..0d48bce0 100644 --- a/rails_application/test/client_orders/order_expired_test.rb +++ b/rails_application/test/client_orders/order_expired_test.rb @@ -34,7 +34,10 @@ def test_order_expired data: { order_id: order_id, product_id: product_id, - price: 39 + base_price: 39, + price: 39, + base_total_value: 39, + total_value: 39, } ) ) diff --git a/rails_application/test/orders/broadcast_test.rb b/rails_application/test/orders/broadcast_test.rb index 32060cbb..55abc45a 100644 --- a/rails_application/test/orders/broadcast_test.rb +++ b/rails_application/test/orders/broadcast_test.rb @@ -50,7 +50,10 @@ def test_broadcast_add_item_to_basket data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -84,7 +87,10 @@ def test_broadcast_remove_item_from_basket data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -95,7 +101,10 @@ def test_broadcast_remove_item_from_basket data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 0, + total_value: 0, } ) ) @@ -131,7 +140,10 @@ def test_broadcast_update_order_value data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -140,7 +152,10 @@ def test_broadcast_update_order_value data: { order_id: order_1_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) @@ -199,7 +214,10 @@ def test_broadcast_update_discount data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -208,7 +226,10 @@ def test_broadcast_update_discount data: { order_id: order_1_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) diff --git a/rails_application/test/orders/item_added_to_basket_test.rb b/rails_application/test/orders/item_added_to_basket_test.rb index 55135c77..f43a5024 100644 --- a/rails_application/test/orders/item_added_to_basket_test.rb +++ b/rails_application/test/orders/item_added_to_basket_test.rb @@ -27,7 +27,10 @@ def test_add_new_item data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 49, + total_value: 49, } ) event_store.publish(item_added_to_basket) @@ -71,7 +74,10 @@ def test_add_the_same_item_2nd_time data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 49, + total_value: 49, } ) ) @@ -81,7 +87,10 @@ def test_add_the_same_item_2nd_time data: { order_id: order_id, product_id: product_id, + base_price: 49, price: 49, + base_total_value: 98, + total_value: 98, } ) ) @@ -140,7 +149,10 @@ def test_add_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -150,7 +162,10 @@ def test_add_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) diff --git a/rails_application/test/orders/item_removed_from_basket_test.rb b/rails_application/test/orders/item_removed_from_basket_test.rb index dd82e52b..76780be5 100644 --- a/rails_application/test/orders/item_removed_from_basket_test.rb +++ b/rails_application/test/orders/item_removed_from_basket_test.rb @@ -30,7 +30,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -39,7 +42,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) @@ -47,7 +53,10 @@ def test_remove_item_when_quantity_gt_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) event_store.publish(item_removed_from_basket) @@ -86,7 +95,10 @@ def test_remove_item_when_quantity_eq_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -95,7 +107,10 @@ def test_remove_item_when_quantity_eq_1 data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 0, + total_value: 0, } ) ) @@ -145,7 +160,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 20, + total_value: 20, } ) ) @@ -154,7 +172,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) @@ -163,7 +184,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 60, + total_value: 60, } ) ) @@ -172,7 +196,10 @@ def test_remove_item_when_there_is_another_item data: { order_id: order_id, product_id: another_product_id, + base_price: 20, price: 20, + base_total_value: 40, + total_value: 40, } ) ) diff --git a/rails_application/test/orders/order_expired_test.rb b/rails_application/test/orders/order_expired_test.rb index cc1c3452..46c91602 100644 --- a/rails_application/test/orders/order_expired_test.rb +++ b/rails_application/test/orders/order_expired_test.rb @@ -31,7 +31,10 @@ def test_expire_created_order data: { order_id: order_id, product_id: product_id, - price: 39 + base_price: 39, + price: 39, + base_total_value: 39, + total_value: 39, } ) ) diff --git a/rails_application/test/orders/order_placed_test.rb b/rails_application/test/orders/order_placed_test.rb index a8a32a68..b08347ad 100644 --- a/rails_application/test/orders/order_placed_test.rb +++ b/rails_application/test/orders/order_placed_test.rb @@ -29,7 +29,10 @@ def test_create_when_not_exists data: { order_id: order_id, product_id: product_id, - price: 20 + base_price: 20, + price: 20, + base_total_value: 20, + total_value: 20, } ), Pricing::OfferAccepted.new( diff --git a/rails_application/test/orders/remove_time_promotion_discount_test.rb b/rails_application/test/orders/remove_time_promotion_discount_test.rb index 6d2de31f..9867282e 100644 --- a/rails_application/test/orders/remove_time_promotion_discount_test.rb +++ b/rails_application/test/orders/remove_time_promotion_discount_test.rb @@ -38,14 +38,15 @@ def test_does_not_removes_time_promotion_when_removing_general_discount private def item_added_to_basket(order_id, product_id) - event_store.publish(Pricing::PriceItemAdded.new(data: { product_id: product_id, order_id: order_id, price: 50 })) + event_store.publish(Pricing::PriceItemAdded.new( + data: { product_id: product_id, order_id: order_id, price: 50, base_price: 50, base_total_value: 50, total_value: 50 })) end def prepare_product(product_id) run_command( ProductCatalog::RegisterProduct.new( product_id: product_id, - ) + ) ) run_command( ProductCatalog::NameProduct.new( diff --git a/rails_application/test/orders/update_order_total_value_test.rb b/rails_application/test/orders/update_order_total_value_test.rb index b8ba844d..9318c01c 100644 --- a/rails_application/test/orders/update_order_total_value_test.rb +++ b/rails_application/test/orders/update_order_total_value_test.rb @@ -36,7 +36,8 @@ def test_newest_event_is_always_applied private def item_added_to_basket(order_id, product_id) - event_store.publish(Pricing::PriceItemAdded.new(data: { product_id: product_id, order_id: order_id, price: 50 })) + event_store.publish(Pricing::PriceItemAdded.new( + data: { product_id: product_id, order_id: order_id, price: 50, base_price: 50, base_total_value: 50, total_value: 50 })) end def prepare_product(product_id) diff --git a/rails_application/test/orders/update_time_promotion_discount_value_test.rb b/rails_application/test/orders/update_time_promotion_discount_value_test.rb index 5a4ea1a7..5ecd7444 100644 --- a/rails_application/test/orders/update_time_promotion_discount_value_test.rb +++ b/rails_application/test/orders/update_time_promotion_discount_value_test.rb @@ -22,7 +22,7 @@ def test_updates_time_promotion_discount_value def item_added_to_basket(order_id, product_id) event_store.publish(Pricing::PriceItemAdded.new(data: { - product_id: product_id, order_id: order_id, price: 50 + product_id: product_id, order_id: order_id, price: 50, base_price: 50, base_total_value: 50, total_value: 50 })) end From db251d0679c4e4df4163e36e423d11bc9ac5e33f Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 08:14:40 +0200 Subject: [PATCH 08/27] Three plus one gratis alternative implementation --- ecommerce/pricing/lib/pricing/discounts.rb | 13 ++ ecommerce/pricing/lib/pricing/events.rb | 6 + ecommerce/pricing/lib/pricing/offer.rb | 15 ++ ecommerce/pricing/test/three_plus_one_test.rb | 140 ++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 ecommerce/pricing/test/three_plus_one_test.rb diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index e72c4976..84f299bf 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -60,5 +60,18 @@ def add(other_discount) def exists? end end + + class ThreePlusOneGratis + def initialize(list) + @list = list + end + + def apply(_base_price) + quantities = @list.quantities.select { |h| h[:quantity] > 3 }.each do + @list.set_free(_1[:product_id]) + end + quantities.any? + end + end end end diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index 5b7928bd..bcee0329 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -56,6 +56,12 @@ class PriceItemRemoved < Infra::Event attribute :total_value, Infra::Types::Price end + class DiscountApplied < Infra::Event + attribute :order_id, Infra::Types::UUID + attribute :base_total_value, Infra::Types::Price + attribute :total_value, Infra::Types::Price + end + class PercentageDiscountRemoved < Infra::Event attribute :order_id, Infra::Types::UUID attribute :type, Infra::Types::String diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index daf9afd7..3d75a406 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -10,6 +10,8 @@ def initialize(id) @list = List.new @discounts = [] @state = :draft + + @three_plus_one_gratis = Discounts::ThreePlusOneGratis.new(@list) end def add_item(product_id, base_price) @@ -24,6 +26,15 @@ def add_item(product_id, base_price) total_value: @list.actual_sum + price } ) + + anything_applied = @three_plus_one_gratis.apply(base_price) + apply DiscountApplied.new( + data: { + order_id: @id, + base_total_value: @list.base_sum, + total_value: @list.actual_sum + } + ) if anything_applied end def remove_item(product_id) @@ -162,6 +173,10 @@ def expire private + on DiscountApplied do |event| + # TODO: Something about discount? + end + on PriceItemAdded do |event| @list.add_item(event.data.fetch(:product_id), event.data.fetch(:base_price), event.data.fetch(:price)) end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb new file mode 100644 index 00000000..f9eac037 --- /dev/null +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -0,0 +1,140 @@ +require_relative "test_helper" + +module Pricing + class ThreePlusOneTest < Test + cover "Pricing::Offer*" + + def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__item_is_free + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 20, + total_value: 20, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 20, + total_amount: 20 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 1, + amount: 20, + discounted_amount: 20, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 40, + total_value: 40, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 40, + total_amount: 40 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 2, + amount: 40, + discounted_amount: 40, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 60, + total_value: 60, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 60, + total_amount: 60 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 3, + amount: 60, + discounted_amount: 60, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 80, + total_value: 80, + } + ), + DiscountApplied.new( + data: { + order_id: order_id, + base_total_value: 80, + total_value: 60, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 80, + total_amount: 80 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 4, + amount: 80, + discounted_amount: 80, + } + ) + ) { add_item(order_id, product_id) } + end + end +end From 92c1405a7a0613889c1e46ab9536dac781df026c Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 09:25:49 +0200 Subject: [PATCH 09/27] Take list into apply method --- ecommerce/pricing/lib/pricing/discounts.rb | 10 +++------- ecommerce/pricing/lib/pricing/offer.rb | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index 84f299bf..de6866d5 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -62,13 +62,9 @@ def exists? end class ThreePlusOneGratis - def initialize(list) - @list = list - end - - def apply(_base_price) - quantities = @list.quantities.select { |h| h[:quantity] > 3 }.each do - @list.set_free(_1[:product_id]) + def apply(list) + quantities = list.quantities.select { |h| h[:quantity] > 3 }.each do + list.set_free(_1[:product_id]) end quantities.any? end diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 3d75a406..a4fe047b 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -11,7 +11,7 @@ def initialize(id) @discounts = [] @state = :draft - @three_plus_one_gratis = Discounts::ThreePlusOneGratis.new(@list) + @three_plus_one_gratis = Discounts::ThreePlusOneGratis.new end def add_item(product_id, base_price) @@ -26,8 +26,8 @@ def add_item(product_id, base_price) total_value: @list.actual_sum + price } ) - - anything_applied = @three_plus_one_gratis.apply(base_price) + + anything_applied = @three_plus_one_gratis.apply(@list) apply DiscountApplied.new( data: { order_id: @id, From 60625e6733c4d1d798eec120c735883b3938d237 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 10:24:30 +0200 Subject: [PATCH 10/27] Three plus one as promotion seems like it is something slightly different than discount it is a promotion? policy? when you get 3 items of same type, 4th one is free --- ecommerce/pricing/lib/pricing/discounts.rb | 9 +- ecommerce/pricing/lib/pricing/events.rb | 6 - ecommerce/pricing/lib/pricing/offer.rb | 24 +--- ecommerce/pricing/test/three_plus_one_test.rb | 129 +++++++++++++++++- 4 files changed, 135 insertions(+), 33 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index de6866d5..489b9d04 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -62,11 +62,10 @@ def exists? end class ThreePlusOneGratis - def apply(list) - quantities = list.quantities.select { |h| h[:quantity] > 3 }.each do - list.set_free(_1[:product_id]) - end - quantities.any? + def apply(product_quantities, product_id, base_price) + product = product_quantities.find { |product_quantity| product_quantity[:product_id] == product_id } + return base_price if product.nil? + product[:quantity] == 3 ? 0 : base_price end end end diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index bcee0329..5b7928bd 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -56,12 +56,6 @@ class PriceItemRemoved < Infra::Event attribute :total_value, Infra::Types::Price end - class DiscountApplied < Infra::Event - attribute :order_id, Infra::Types::UUID - attribute :base_total_value, Infra::Types::Price - attribute :total_value, Infra::Types::Price - end - class PercentageDiscountRemoved < Infra::Event attribute :order_id, Infra::Types::UUID attribute :type, Infra::Types::String diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index a4fe047b..d35e9a25 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -10,12 +10,15 @@ def initialize(id) @list = List.new @discounts = [] @state = :draft - - @three_plus_one_gratis = Discounts::ThreePlusOneGratis.new end - def add_item(product_id, base_price) - price = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(base_price) + def add_item(product_id, base_price, promotion = Pricing::Discounts::ThreePlusOneGratis.new) + if promotion + price = promotion.apply(@list.quantities, product_id, base_price) + else + price = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(base_price) + end + apply PriceItemAdded.new( data: { order_id: @id, @@ -26,15 +29,6 @@ def add_item(product_id, base_price) total_value: @list.actual_sum + price } ) - - anything_applied = @three_plus_one_gratis.apply(@list) - apply DiscountApplied.new( - data: { - order_id: @id, - base_total_value: @list.base_sum, - total_value: @list.actual_sum - } - ) if anything_applied end def remove_item(product_id) @@ -173,10 +167,6 @@ def expire private - on DiscountApplied do |event| - # TODO: Something about discount? - end - on PriceItemAdded do |event| @list.add_item(event.data.fetch(:product_id), event.data.fetch(:base_price), event.data.fetch(:price)) end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index f9eac037..95b564b3 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -106,14 +106,133 @@ def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__ite order_id: order_id, product_id: product_id, base_price: 20, - price: 20, + price: 0, base_total_value: 80, - total_value: 80, + total_value: 60, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 60, + total_amount: 80 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 4, + amount: 80, + discounted_amount: 60, + } + ) + ) { add_item(order_id, product_id) } + end + + def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_includes_both_discounts + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 20, + total_value: 20, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 20, + total_amount: 20 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 1, + amount: 20, + discounted_amount: 20, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 40, + total_value: 40, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 40, + total_amount: 40 } ), - DiscountApplied.new( + PriceItemValueCalculated.new( data: { order_id: order_id, + product_id: product_id, + quantity: 2, + amount: 40, + discounted_amount: 40, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 60, + total_value: 60, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 60, + total_amount: 60 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 3, + amount: 60, + discounted_amount: 60, + } + ) + ) { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 0, base_total_value: 80, total_value: 60, } @@ -121,7 +240,7 @@ def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__ite OrderTotalValueCalculated.new( data: { order_id: order_id, - discounted_amount: 80, + discounted_amount: 60, total_amount: 80 } ), @@ -131,7 +250,7 @@ def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__ite product_id: product_id, quantity: 4, amount: 80, - discounted_amount: 80, + discounted_amount: 60, } ) ) { add_item(order_id, product_id) } From fbca2b9d7eed6442150ffe9e3054431efc03bf6c Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 12:28:03 +0200 Subject: [PATCH 11/27] add command to helper --- ecommerce/pricing/test/test_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ecommerce/pricing/test/test_helper.rb b/ecommerce/pricing/test/test_helper.rb index 4830dd82..94f1396c 100644 --- a/ecommerce/pricing/test/test_helper.rb +++ b/ecommerce/pricing/test/test_helper.rb @@ -46,6 +46,10 @@ def set_time_promotion_discount(order_id, amount) run_command(SetTimePromotionDiscount.new(order_id: order_id, amount: amount)) end + def set_percentage_discount(order_id, amount) + run_command(SetPercentageDiscount.new(order_id: order_id, amount: amount)) + end + def remove_time_promotion_discount(order_id) run_command(RemoveTimePromotionDiscount.new(order_id: order_id)) end From 464d64a7c3c26ace8ef8ed0c1e7730022862f01b Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 12:28:31 +0200 Subject: [PATCH 12/27] Failing test -- Discount added at the end should be included correctly --- ecommerce/pricing/test/three_plus_one_test.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index 95b564b3..b036e9dd 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -254,6 +254,23 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ } ) ) { add_item(order_id, product_id) } + assert_events( + stream, + PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT, + amount: 10 + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + discounted_amount: 54, + total_amount: 80 + } + ) + ) { set_percentage_discount(order_id, 10) } end end end From 50450fa7ddd4ab913912e24438471c95ef1f239f Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 12:34:11 +0200 Subject: [PATCH 13/27] Extend PercentageDiscountSet event by total and base values --- ecommerce/pricing/lib/pricing/events.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index 5b7928bd..63e1fbbd 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -36,6 +36,8 @@ class PercentageDiscountSet < Infra::Event attribute :order_id, Infra::Types::UUID attribute :type, Infra::Types::String attribute :amount, Infra::Types::PercentageDiscount + attribute :base_total_value, Infra::Types::Price + attribute :total_value, Infra::Types::Price end class PriceItemAdded < Infra::Event From 87ee904d13e7a428fd79b8e67944fef823f49a89 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 12:34:27 +0200 Subject: [PATCH 14/27] Set base_total_value and total_value on DiscountSet event --- ecommerce/pricing/lib/pricing/offer.rb | 4 +++- ecommerce/pricing/test/three_plus_one_test.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index d35e9a25..287612b9 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -51,7 +51,9 @@ def apply_discount(discount) data: { order_id: @id, type: discount.type, - amount: discount.value + amount: discount.value, + base_total_value: @list.base_sum, + total_value: @list.actual_sum - @list.actual_sum * (discount.value / 100) } ) end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index b036e9dd..b552f543 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -260,7 +260,9 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ data: { order_id: order_id, type: Discounts::GENERAL_DISCOUNT, - amount: 10 + amount: 10, + base_total_value: 80, + total_value: 54 } ), OrderTotalValueCalculated.new( From 677123444998d274e69dbdce66817400febe89b0 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 13:00:15 +0200 Subject: [PATCH 15/27] Promotion should be off by default --- ecommerce/pricing/lib/pricing/offer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 287612b9..72643c6a 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -12,7 +12,7 @@ def initialize(id) @state = :draft end - def add_item(product_id, base_price, promotion = Pricing::Discounts::ThreePlusOneGratis.new) + def add_item(product_id, base_price, promotion = nil) if promotion price = promotion.apply(@list.quantities, product_id, base_price) else From b4d2e9224e1d871868737b82fbc909178b58a3f2 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 13:00:23 +0200 Subject: [PATCH 16/27] Skip test, it doesn't fit into new design Arrange section isn't correct, there's no real items in the order --- ecommerce/pricing/test/apply_time_promotion_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb index 87a9ff64..e72abf2a 100644 --- a/ecommerce/pricing/test/apply_time_promotion_test.rb +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -5,6 +5,8 @@ class ApplyTimePromotionTest < Test cover "Pricing*" def test_applies_biggest_time_promotion_discount + skip 'This test is skipped because it doesnt fit into new design.' + order_id = SecureRandom.uuid product_id = SecureRandom.uuid create_inactive_time_promotion(60) From 795c2d3d1b82ad9ff4bafd3731fa64df732ddaaf Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 13:00:51 +0200 Subject: [PATCH 17/27] Adjust PercentageDiscountSet event's data in test --- ecommerce/pricing/test/apply_time_promotion_test.rb | 12 +++++++----- ecommerce/pricing/test/pricing_test.rb | 12 +++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb index e72abf2a..1d9f9fb6 100644 --- a/ecommerce/pricing/test/apply_time_promotion_test.rb +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -63,10 +63,10 @@ def item_added_to_basket_event(order_id, product_id) data: { product_id: product_id, order_id: order_id, - base_price: 1111, - price: 1111, - base_total_value: 1111, - total_value: 1111 + base_price: 1000, + price: 1000, + base_total_value: 1000, + total_value: 1000 } ) end @@ -80,7 +80,9 @@ def percentage_discount_set_event(order_id, amount) data: { order_id: order_id, type: Pricing::Discounts::TIME_PROMOTION_DISCOUNT, - amount: amount + amount: amount, + base_total_value: 1000, + total_value: 500 } ) end diff --git a/ecommerce/pricing/test/pricing_test.rb b/ecommerce/pricing/test/pricing_test.rb index fc9cd034..3dbd955a 100644 --- a/ecommerce/pricing/test/pricing_test.rb +++ b/ecommerce/pricing/test/pricing_test.rb @@ -103,7 +103,9 @@ def test_sets_time_promotion_discount data: { order_id: order_id, type: Discounts::TIME_PROMOTION_DISCOUNT, - amount: 25 + amount: 25, + base_total_value: 0, + total_value: 0 } ) ) { set_time_promotion_discount(order_id, 25) } @@ -162,7 +164,9 @@ def test_calculates_total_value_with_discount data: { order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 10 + amount: 10, + base_total_value: 20, + total_value: 18 } ), OrderTotalValueCalculated.new( @@ -232,7 +236,9 @@ def test_calculates_total_value_with_100_discount data: { order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 100 + amount: 100, + base_total_value: 20, + total_value: 0 } ), OrderTotalValueCalculated.new( From e10e3ad8ce34844aa3af7f552805037ad488bbdc Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 13:14:29 +0200 Subject: [PATCH 18/27] Extend the add item command for now to continue poc --- ecommerce/pricing/lib/pricing/commands.rb | 1 + ecommerce/pricing/lib/pricing/services.rb | 3 ++- ecommerce/pricing/test/test_helper.rb | 4 ++-- ecommerce/pricing/test/three_plus_one_test.rb | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/commands.rb b/ecommerce/pricing/lib/pricing/commands.rb index 984b0fb0..6f1b47cd 100644 --- a/ecommerce/pricing/lib/pricing/commands.rb +++ b/ecommerce/pricing/lib/pricing/commands.rb @@ -3,6 +3,7 @@ class AddPriceItem < Infra::Command attribute :order_id, Infra::Types::UUID attribute :product_id, Infra::Types::UUID attribute :price, Infra::Types::Price + attribute :promotion, Infra::Types::Strict::Bool.default(false) alias aggregate_id order_id end diff --git a/ecommerce/pricing/lib/pricing/services.rb b/ecommerce/pricing/lib/pricing/services.rb index 4744fcc0..055e3e86 100644 --- a/ecommerce/pricing/lib/pricing/services.rb +++ b/ecommerce/pricing/lib/pricing/services.rb @@ -120,7 +120,8 @@ def initialize(event_store) def call(command) @repository.with_aggregate(Offer, command.aggregate_id) do |order| - order.add_item(command.product_id, command.price) + promotion = command.promotion ? Discounts::ThreePlusOneGratis.new : nil + order.add_item(command.product_id, command.price, promotion) end end end diff --git a/ecommerce/pricing/test/test_helper.rb b/ecommerce/pricing/test/test_helper.rb index 94f1396c..d6d077a8 100644 --- a/ecommerce/pricing/test/test_helper.rb +++ b/ecommerce/pricing/test/test_helper.rb @@ -26,9 +26,9 @@ def calculate_total_value(order_id) run_command(CalculateTotalValue.new(order_id: order_id)) end - def add_item(order_id, product_id) + def add_item(order_id, product_id, promotion: false) run_command( - AddPriceItem.new(order_id: order_id, product_id: product_id, price: find_price(product_id)) + AddPriceItem.new(order_id: order_id, product_id: product_id, price: find_price(product_id), promotion:) ) end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index b552f543..7a8874e5 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -127,7 +127,7 @@ def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__ite discounted_amount: 60, } ) - ) { add_item(order_id, product_id) } + ) { add_item(order_id, product_id, promotion: true) } end def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_includes_both_discounts @@ -253,7 +253,7 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ discounted_amount: 60, } ) - ) { add_item(order_id, product_id) } + ) { add_item(order_id, product_id, promotion: true) } assert_events( stream, PercentageDiscountSet.new( From b24f5104ef4363fa33a7f580448df0bb4097b025 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 13:31:37 +0200 Subject: [PATCH 19/27] Adjust tests to match the event structure --- rails_application/test/orders/broadcast_test.rb | 8 ++++++-- rails_application/test/orders/discount_test.rb | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/rails_application/test/orders/broadcast_test.rb b/rails_application/test/orders/broadcast_test.rb index 55abc45a..75c1cdea 100644 --- a/rails_application/test/orders/broadcast_test.rb +++ b/rails_application/test/orders/broadcast_test.rb @@ -239,7 +239,9 @@ def test_broadcast_update_discount data: { order_id: order_1_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 30 + amount: 30, + base_total_value: 40, + total_value: 28 } ) ) @@ -251,7 +253,9 @@ def test_broadcast_update_discount data: { order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 30 + amount: 30, + base_total_value: 20, + total_value: 14 } ) ) diff --git a/rails_application/test/orders/discount_test.rb b/rails_application/test/orders/discount_test.rb index 5f188196..9a85c626 100644 --- a/rails_application/test/orders/discount_test.rb +++ b/rails_application/test/orders/discount_test.rb @@ -86,7 +86,9 @@ def test_newest_event_is_always_applied data: { order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 30 + amount: 30, + base_total_value: 50, + total_value: 35 }, metadata: { timestamp: Time.current @@ -96,7 +98,9 @@ def test_newest_event_is_always_applied data: { order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 20 + amount: 20, + base_total_value: 50, + total_value: 40 }, metadata: { timestamp: 1.minute.ago From 5ed78dca576c76230512e688ed9098758da07b90 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 14:10:30 +0200 Subject: [PATCH 20/27] Don't change price of free product --- ecommerce/pricing/lib/pricing/offer.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 72643c6a..179598cf 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -244,11 +244,16 @@ def remove_item(product_id, price) def apply_discounts(discounts) @items = @items.map do |item| + next item if is_free?(item) price = discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(item.base_price) item.with(price:) end end + def is_free?(item) + item.price == 0 + end + def contains_free_products? @items.any? { |item| item.price == 0 } end From b428ce2acbed6d5b6246b1a29f71bffd4e6891af Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 14:10:44 +0200 Subject: [PATCH 21/27] Assert PriceItemValueCalculated --- ecommerce/pricing/test/three_plus_one_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index 7a8874e5..bbe45b12 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -271,6 +271,15 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ discounted_amount: 54, total_amount: 80 } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 4, + amount: 80, + discounted_amount: 54, + } ) ) { set_percentage_discount(order_id, 10) } end From c281497188ff0d8a753ee3e5afae94746b2a6239 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 14:14:58 +0200 Subject: [PATCH 22/27] Keep info about applied promotion it will be easier to revert it if that's the case --- ecommerce/pricing/lib/pricing/events.rb | 1 + ecommerce/pricing/lib/pricing/offer.rb | 22 ++++++++++--------- ecommerce/pricing/test/three_plus_one_test.rb | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index 63e1fbbd..89e17c66 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -47,6 +47,7 @@ class PriceItemAdded < Infra::Event attribute :price, Infra::Types::Price attribute :base_total_value, Infra::Types::Price attribute :total_value, Infra::Types::Price + attribute? :applied_promotion, Infra::Types::String end class PriceItemRemoved < Infra::Event diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 179598cf..703a081f 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -19,16 +19,18 @@ def add_item(product_id, base_price, promotion = nil) price = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(base_price) end - apply PriceItemAdded.new( - data: { - order_id: @id, - product_id: product_id, - base_price: base_price, - price: price, - base_total_value: @list.base_sum + base_price, - total_value: @list.actual_sum + price - } - ) + data = { + order_id: @id, + product_id: product_id, + base_price: base_price, + price: price, + base_total_value: @list.base_sum + base_price, + total_value: @list.actual_sum + price, + } + + data[:applied_promotion] = promotion.class.name if promotion + + apply PriceItemAdded.new(data:) end def remove_item(product_id) diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index bbe45b12..dad99782 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -109,6 +109,7 @@ def test_given_three_items_are_added_when_forth_item_is_added_then_the_last__ite price: 0, base_total_value: 80, total_value: 60, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s } ), OrderTotalValueCalculated.new( @@ -235,6 +236,7 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ price: 0, base_total_value: 80, total_value: 60, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s } ), OrderTotalValueCalculated.new( From b49a1f3641966d483c25513b9f2643faa39fcd2d Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 20:29:31 +0200 Subject: [PATCH 23/27] Support additional cases 1. When 4 products are added (one is free) and 5th one is added 2. When 7 producs are added (one is free) and 8th is added as free --- ecommerce/pricing/lib/pricing/discounts.rb | 6 +- ecommerce/pricing/lib/pricing/offer.rb | 10 +- ecommerce/pricing/test/three_plus_one_test.rb | 110 ++++++++++++++++++ 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index 489b9d04..11407e4f 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -64,8 +64,10 @@ def exists? class ThreePlusOneGratis def apply(product_quantities, product_id, base_price) product = product_quantities.find { |product_quantity| product_quantity[:product_id] == product_id } - return base_price if product.nil? - product[:quantity] == 3 ? 0 : base_price + return [false, price: base_price] if product.nil? + quantity = product[:quantity] + 1 + return [false, base_price] if quantity < 4 + return [true, 0] if quantity % 4 == 0 end end end diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 703a081f..b3ea2f0d 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -14,8 +14,10 @@ def initialize(id) def add_item(product_id, base_price, promotion = nil) if promotion - price = promotion.apply(@list.quantities, product_id, base_price) - else + promotion_applies, price = promotion.apply(@list.quantities, product_id, base_price) + end + + unless promotion_applies price = @discounts.inject(Discounts::NoPercentageDiscount.new, :add).apply(base_price) end @@ -27,8 +29,8 @@ def add_item(product_id, base_price, promotion = nil) base_total_value: @list.base_sum + base_price, total_value: @list.actual_sum + price, } - - data[:applied_promotion] = promotion.class.name if promotion + + data[:applied_promotion] = promotion.class.name if promotion_applies apply PriceItemAdded.new(data:) end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index dad99782..319e172b 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -285,5 +285,115 @@ def test_given_3_plus_one__when_10_percent_discount_for_offer__then_offer_price_ ) ) { set_percentage_discount(order_id, 10) } end + + def test_given_three_plus_one_promotion_when_five_items_are_added_then_only_one_item_is_free + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 3.times { add_item(order_id, product_id, promotion: true) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 0, + base_total_value: 80, + total_value: 60, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 80, + discounted_amount: 60 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 4, + amount: 80, + discounted_amount: 60, + } + ) + ) { add_item(order_id, product_id, promotion: true) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 100, + total_value: 80, + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 100, + discounted_amount: 80 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 5, + amount: 100, + discounted_amount: 80, + } + ) + ) { add_item(order_id, product_id, promotion: true) } + end + + def test_given_three_plus_one_promotion_when_eight_items_are_added_then_two_items_are_free + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 7.times { add_item(order_id, product_id, promotion: true) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 0, + base_total_value: 160, + total_value: 120, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 160, + discounted_amount: 120 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 8, + amount: 160, + discounted_amount: 120, + } + ) + ) { add_item(order_id, product_id, promotion: true) } + end end end From f332616b904ea3f59198adb91c8521b283fccf20 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Thu, 17 Apr 2025 20:49:08 +0200 Subject: [PATCH 24/27] Test when 3+1 and remove item --- ecommerce/pricing/test/three_plus_one_test.rb | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index 319e172b..992d93bb 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -395,5 +395,44 @@ def test_given_three_plus_one_promotion_when_eight_items_are_added_then_two_item ) ) { add_item(order_id, product_id, promotion: true) } end + + def test_given_three_plus_one_is_applied_when_item_is_removed_then_the_discount_is_removed + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 4.times { add_item(order_id, product_id, promotion: true) } + + assert_events( + stream, + PriceItemRemoved.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 0, + base_total_value: 60, + total_value: 60 + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 60, + discounted_amount: 60 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 3, + amount: 60, + discounted_amount: 60, + } + ) + ) { remove_item(order_id, product_id) } + end end end From 93687865f2c6e5133f4475358dc30da968a08987 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Fri, 18 Apr 2025 08:37:52 +0200 Subject: [PATCH 25/27] Kill mutants --- ecommerce/pricing/lib/pricing/discounts.rb | 10 +- ecommerce/pricing/lib/pricing/services.rb | 2 +- ecommerce/pricing/test/discounts_test.rb | 50 +++++++ ecommerce/pricing/test/three_plus_one_test.rb | 136 ++++++++++++++++++ 4 files changed, 192 insertions(+), 6 deletions(-) diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index 11407e4f..3c18b604 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -63,11 +63,11 @@ def exists? class ThreePlusOneGratis def apply(product_quantities, product_id, base_price) - product = product_quantities.find { |product_quantity| product_quantity[:product_id] == product_id } - return [false, price: base_price] if product.nil? - quantity = product[:quantity] + 1 - return [false, base_price] if quantity < 4 - return [true, 0] if quantity % 4 == 0 + product = product_quantities.find { |product_quantity| product_quantity.fetch(:product_id) == product_id } + return [false, base_price] if product.nil? + quantity = product.fetch(:quantity) + 1 + return [false, base_price] if !quantity.%(4).eql?(0) + [true, 0] end end end diff --git a/ecommerce/pricing/lib/pricing/services.rb b/ecommerce/pricing/lib/pricing/services.rb index 055e3e86..ed5d7d86 100644 --- a/ecommerce/pricing/lib/pricing/services.rb +++ b/ecommerce/pricing/lib/pricing/services.rb @@ -120,7 +120,7 @@ def initialize(event_store) def call(command) @repository.with_aggregate(Offer, command.aggregate_id) do |order| - promotion = command.promotion ? Discounts::ThreePlusOneGratis.new : nil + promotion = Discounts::ThreePlusOneGratis.new if command.promotion order.add_item(command.product_id, command.price, promotion) end end diff --git a/ecommerce/pricing/test/discounts_test.rb b/ecommerce/pricing/test/discounts_test.rb index b7387cc7..ad8f6a7b 100644 --- a/ecommerce/pricing/test/discounts_test.rb +++ b/ecommerce/pricing/test/discounts_test.rb @@ -64,5 +64,55 @@ def test_doesnt_change_total assert_equal(100, NoPercentageDiscount.new.apply(100)) end end + + class ThreePlusOneGratisTest < Test + cover "Pricing::Discounts*" + def setup + @three_plus_one_gratis = ThreePlusOneGratis.new + @product_id = SecureRandom.uuid + end + + def test_returns_full_price_when_product_not_found + result = @three_plus_one_gratis.apply([], @product_id, 20) + assert_equal [false, 20], result + end + + def test_returns_full_price_when_quantity_plus_one_is_less_than_4 + quantities = [{ product_id: @product_id, quantity: 2 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [false, 20], result + end + + def test_returns_free_when_quantity_plus_one_is_4 + quantities = [{ product_id: @product_id, quantity: 3 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [true, 0], result + end + + def test_returns_full_price_when_quantity_plus_one_is_5 + quantities = [{ product_id: @product_id, quantity: 4 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [false, 20], result + end + + def test_returns_free_when_quantity_plus_one_is_8 + quantities = [{ product_id: @product_id, quantity: 7 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [true, 0], result + end + + def test_returns_full_price_when_quantity_plus_one_is_9 + quantities = [{ product_id: @product_id, quantity: 8 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [false, 20], result + end + + def test_does_not_confuse_other_product_ids + other_product_id = SecureRandom.uuid + quantities = [{ product_id: other_product_id, quantity: 3 }] + result = @three_plus_one_gratis.apply(quantities, @product_id, 20) + assert_equal [false, 20], result + end + end end end diff --git a/ecommerce/pricing/test/three_plus_one_test.rb b/ecommerce/pricing/test/three_plus_one_test.rb index 992d93bb..e96ace03 100644 --- a/ecommerce/pricing/test/three_plus_one_test.rb +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -434,5 +434,141 @@ def test_given_three_plus_one_is_applied_when_item_is_removed_then_the_discount_ ) ) { remove_item(order_id, product_id) } end + + def test_given_three_items_added_to_basket_and_no_three_plus_one_promotion_when_fourth_is_added_then_price_is_not_discounted + product_id = SecureRandom.uuid + set_price(product_id, 20) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 3.times { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: product_id, + base_price: 20, + price: 20, + base_total_value: 80, + total_value: 80 + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 80, + discounted_amount: 80 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 4, + amount: 80, + discounted_amount: 80, + }), + ) { add_item(order_id, product_id) } + end + + def test_given_three_items_in_basket_when_different_one_is_added_then_price_is_not_discounted + product_id = SecureRandom.uuid + different_product_id = SecureRandom.uuid + set_price(product_id, 20) + set_price(different_product_id, 50) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 3.times { add_item(order_id, product_id) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: different_product_id, + base_price: 50, + price: 50, + base_total_value: 110, + total_value: 110 + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 110, + discounted_amount: 110 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 3, + amount: 60, + discounted_amount: 60, + }), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: different_product_id, + quantity: 1, + amount: 50, + discounted_amount: 50, + }) + ) { add_item(order_id, different_product_id) } + end + + def test_given_two_sets_of_three_items_in_basket_when_added_forth_item_then_price_is_discounted_for_that_item + product_id = SecureRandom.uuid + different_product_id = SecureRandom.uuid + set_price(product_id, 20) + set_price(different_product_id, 50) + order_id = SecureRandom.uuid + stream = "Pricing::Offer$#{order_id}" + + 3.times { add_item(order_id, product_id, promotion: true) } + 3.times { add_item(order_id, different_product_id, promotion: true) } + + assert_events( + stream, + PriceItemAdded.new( + data: { + order_id: order_id, + product_id: different_product_id, + base_price: 50, + price: 0, + base_total_value: 260, + total_value: 210, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 260, + discounted_amount: 210 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_id, + quantity: 3, + amount: 60, + discounted_amount: 60, + }), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: different_product_id, + quantity: 4, + amount: 200, + discounted_amount: 150, + }) + ) { add_item(order_id, different_product_id, promotion: true) } + end end end From 4ab6f342d8937031267116be10c43307da9e644d Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Fri, 18 Apr 2025 08:53:40 +0200 Subject: [PATCH 26/27] Kill mutant --- ecommerce/pricing/lib/pricing/offer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index b3ea2f0d..e2aba86e 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -12,7 +12,7 @@ def initialize(id) @state = :draft end - def add_item(product_id, base_price, promotion = nil) + def add_item(product_id, base_price, promotion) if promotion promotion_applies, price = promotion.apply(@list.quantities, product_id, base_price) end From e18785b6872e759bcfd7528819d6607ffc0aa565 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Fri, 18 Apr 2025 09:40:00 +0200 Subject: [PATCH 27/27] Fix test - kill last mutant --- ecommerce/pricing/test/apply_time_promotion_test.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb index 1d9f9fb6..7edf1674 100644 --- a/ecommerce/pricing/test/apply_time_promotion_test.rb +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -5,7 +5,6 @@ class ApplyTimePromotionTest < Test cover "Pricing*" def test_applies_biggest_time_promotion_discount - skip 'This test is skipped because it doesnt fit into new design.' order_id = SecureRandom.uuid product_id = SecureRandom.uuid @@ -59,7 +58,7 @@ def create_active_time_promotion(discount) end def item_added_to_basket_event(order_id, product_id) - Pricing::PriceItemAdded.new( + price_item_added = Pricing::PriceItemAdded.new( data: { product_id: product_id, order_id: order_id, @@ -69,6 +68,8 @@ def item_added_to_basket_event(order_id, product_id) total_value: 1000 } ) + event_store.append(price_item_added, stream_name: stream_name(order_id)) + price_item_added end def set_time_promotion_discount(order_id, discount)