diff --git a/ecommerce/pricing/lib/pricing/calculate_order_total_value.rb b/ecommerce/pricing/lib/pricing/calculate_order_total_value.rb index e09298c0..d91ec427 100644 --- a/ecommerce/pricing/lib/pricing/calculate_order_total_value.rb +++ b/ecommerce/pricing/lib/pricing/calculate_order_total_value.rb @@ -1,14 +1,83 @@ module Pricing class CalculateOrderTotalValue def call(event) - command_bus.(CalculateTotalValue.new(order_id: event.data.fetch(:order_id))) + items = [] + discounts = [] + events = + event_store + .read + .stream("Pricing::Offer$#{event.data.fetch(:order_id)}") + .to_a + events.each do |event| + case event + when PriceItemAdded + items << { + product_id: event.data.fetch(:product_id), + base_price: event.data.fetch(:base_price), + price: event.data.fetch(:base_price) + } + when PriceItemRemoved + index = + items.index do |i| + i[:product_id] == event.data[:product_id] && + i[:price] == event.data[:price] + end + items.delete_at(index) if index + when PercentageDiscountSet + discounts << { + type: event.data.fetch(:type), + amount: event.data.fetch(:amount) + } + when PercentageDiscountChanged + discounts = + discounts.reject do |discount| + discount[:type] == event.data.fetch(:type) + end + discounts << { + type: event.data.fetch(:type), + amount: event.data.fetch(:amount) + } + when PercentageDiscountRemoved + discounts = + discounts.reject do |discount| + discount[:type] == event.data.fetch(:type) + end + when ProductMadeFreeForOrder + item = + items.find do |i| + i[:product_id] == event.data.fetch(:product_id) && i[:price] > 0 + end + item[:price] = 0.0 if item + when FreeProductRemovedFromOrder + item = + items.find do |i| + i[:product_id] == event.data.fetch(:product_id) && i[:price] == 0 + end + item[:price] = item[:base_price] if item + end + end + + total_amount = items.sum { |item| item[:base_price] } + discounted_amount = items.sum { |item| item[:price] } + discounts.each do |discount| + discounted_amount -= discounted_amount * (discount[:amount] / 100) + end + + event_store.publish( + OrderTotalValueCalculated.new( + data: { + order_id: event.data.fetch(:order_id), + total_amount:, + discounted_amount: + } + ) + ) end private - def command_bus - Pricing.command_bus + def event_store + Pricing.event_store end end end - diff --git a/ecommerce/pricing/test/free_products_test.rb b/ecommerce/pricing/test/free_products_test.rb index a2423401..cfa8f0d8 100644 --- a/ecommerce/pricing/test/free_products_test.rb +++ b/ecommerce/pricing/test/free_products_test.rb @@ -13,25 +13,26 @@ def test_making_product_free_possible_when_order_is_eligible add_item(order_id, product_1_id) add_item(order_id, product_1_id) - assert_events_contain( - stream_name(order_id), - ProductMadeFreeForOrder.new( - data: { - order_id: order_id, - product_id: product_1_id - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 60, - total_amount: 80 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 60, total_amount: 80 } ) do - run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) - ) + assert_events_contain( + stream_name(order_id), + ProductMadeFreeForOrder.new( + data: { + order_id: order_id, + product_id: product_1_id + } + ) + ) do + run_command( + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) + ) + end end end @@ -46,25 +47,26 @@ def test_making_only_the_cheapest_product_free add_item(order_id, product_1_id) add_item(order_id, cheaper_product) - assert_events_contain( - stream_name(order_id), - ProductMadeFreeForOrder.new( - data: { - order_id: order_id, - product_id: cheaper_product - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 60, - total_amount: 70 - } - ), + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 60, total_amount: 70 } ) do - run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: cheaper_product) - ) + assert_events_contain( + stream_name(order_id), + ProductMadeFreeForOrder.new( + data: { + order_id: order_id, + product_id: cheaper_product + } + ) + ) do + run_command( + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: cheaper_product + ) + ) + end end end @@ -78,12 +80,18 @@ def test_making_product_free_not_possible_if_is_already_set add_item(order_id, product_1_id) run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) assert_raises FreeProductAlreadyMade do run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) end end @@ -98,40 +106,54 @@ def test_making_product_free_possible_after_previous_free_product_was_removed add_item(order_id, product_1_id) run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) run_command( - Pricing::RemovePriceItem.new(order_id: order_id, product_id: product_1_id) + Pricing::RemovePriceItem.new( + order_id: order_id, + product_id: product_1_id + ) ) run_command( - Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::RemoveFreeProductFromOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) run_command( - Pricing::AddPriceItem.new(order_id: order_id, product_id: product_1_id, price: 20) + Pricing::AddPriceItem.new( + order_id: order_id, + product_id: product_1_id, + price: 20 + ) ) - assert_events_contain( - stream_name(order_id), - ProductMadeFreeForOrder.new( - data: { - order_id: order_id, - product_id: product_1_id - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 60, - total_amount: 80 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 60, total_amount: 80 } ) do - run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) - ) + assert_events_contain( + stream_name(order_id), + ProductMadeFreeForOrder.new( + data: { + order_id: order_id, + product_id: product_1_id + } + ), + ) do + run_command( + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) + ) + end end end @@ -145,28 +167,32 @@ def test_removing_free_product_possible_if_it_is_already_set add_item(order_id, product_1_id) run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) - assert_events_contain( - stream_name(order_id), - FreeProductRemovedFromOrder.new( - data: { - order_id: order_id, - product_id: product_1_id - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 80, - total_amount: 80 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 80, total_amount: 80 } ) do - run_command( - Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_1_id) - ) + assert_events_contain( + stream_name(order_id), + FreeProductRemovedFromOrder.new( + data: { + order_id: order_id, + product_id: product_1_id + } + ) + ) do + run_command( + Pricing::RemoveFreeProductFromOrder.new( + order_id: order_id, + product_id: product_1_id + ) + ) + end end end @@ -178,7 +204,10 @@ def test_removing_free_product_not_possible_if_is_not_set assert_no_events(stream_name(order_id)) do run_command( - Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::RemoveFreeProductFromOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) end end @@ -193,16 +222,25 @@ def test_removing_free_product_twice_not_possible add_item(order_id, product_1_id) run_command( - Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::MakeProductFreeForOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) run_command( - Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::RemoveFreeProductFromOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) assert_no_events(stream_name(order_id)) do run_command( - Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_1_id) + Pricing::RemoveFreeProductFromOrder.new( + order_id: order_id, + product_id: product_1_id + ) ) end end @@ -212,6 +250,5 @@ def test_removing_free_product_twice_not_possible def stream_name(id) "Pricing::Offer$#{id}" end - end end diff --git a/ecommerce/pricing/test/pricing_test.rb b/ecommerce/pricing/test/pricing_test.rb index fc9cd034..a7934e4e 100644 --- a/ecommerce/pricing/test/pricing_test.rb +++ b/ecommerce/pricing/test/pricing_test.rb @@ -21,15 +21,9 @@ def test_calculates_total_value add_item(order_id, product_1_id) add_item(order_id, product_2_id) stream = stream_name(order_id) - assert_events( - stream, - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 50, - total_amount: 50 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 50, total_amount: 50 } ) { calculate_total_value(order_id) } end @@ -146,15 +140,9 @@ def test_calculates_total_value_with_discount order_id = SecureRandom.uuid add_item(order_id, product_1_id) stream = stream_name(order_id) - assert_events( - stream, - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 20, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 20, total_amount: 20 } ) { run_command(CalculateTotalValue.new(order_id: order_id)) } assert_events_contain( stream, @@ -164,59 +152,58 @@ def test_calculates_total_value_with_discount type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 10 } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 18, - total_amount: 20 - } ) ) do run_command( - Pricing::SetPercentageDiscount.new(order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 10) - ) - end - assert_events_contain( - stream, - PercentageDiscountChanged.new( - data: { + Pricing::SetPercentageDiscount.new( order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 50 - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 10, - total_amount: 20 - } + amount: 10 + ) ) + end + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 10, total_amount: 20 } ) do - run_command( - Pricing::ChangePercentageDiscount.new(order_id: order_id, amount: 50) - ) + assert_events_contain( + stream, + PercentageDiscountChanged.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 50, + } + ) + ) do + run_command( + Pricing::ChangePercentageDiscount.new( + order_id: order_id, + amount: 50 + ) + ) + end end - assert_events_contain( - stream, - PercentageDiscountRemoved.new( - data: { - order_id: order_id, - type: Pricing::Discounts::GENERAL_DISCOUNT - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 20, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 20, total_amount: 20 } ) do - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT) - ) + assert_events_contain( + stream, + PercentageDiscountRemoved.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + } + ) + ) do + run_command( + Pricing::RemovePercentageDiscount.new( + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT + ) + ) + end end end @@ -226,26 +213,24 @@ def test_calculates_total_value_with_100_discount order_id = SecureRandom.uuid add_item(order_id, product_1_id) stream = stream_name(order_id) - assert_events_contain( - stream, - PercentageDiscountSet.new( - data: { - order_id: order_id, - type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 100 - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 0, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 0, total_amount: 20 } ) do - run_command( - Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 100) - ) + assert_events_contain( + stream, + PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 100 + } + ) + ) do + run_command( + Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 100) + ) + end end end @@ -304,9 +289,7 @@ def test_changing_discount_not_possible_when_discount_is_removed run_command( Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) ) - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id) - ) + run_command(Pricing::RemovePercentageDiscount.new(order_id: order_id)) assert_raises NotPossibleToChangeDiscount do run_command( @@ -325,26 +308,27 @@ def test_changing_discount_possible_when_discount_is_set Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) ) - assert_events_contain( - stream, - PercentageDiscountChanged.new( - data: { - order_id: order_id, - type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 100 - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 0, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 0, total_amount: 20 } ) do - run_command( - Pricing::ChangePercentageDiscount.new(order_id: order_id, amount: 100) - ) + assert_events_contain( + stream, + PercentageDiscountChanged.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 100, + } + ) + ) do + run_command( + Pricing::ChangePercentageDiscount.new( + order_id: order_id, + amount: 100 + ) + ) + end end end @@ -361,26 +345,27 @@ def test_changing_discount_possible_more_than_once Pricing::ChangePercentageDiscount.new(order_id: order_id, amount: 20) ) - assert_events_contain( - stream, - PercentageDiscountChanged.new( - data: { - order_id: order_id, - type: Pricing::Discounts::GENERAL_DISCOUNT, - amount: 100 - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 0, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 0, total_amount: 20 } ) do - run_command( - Pricing::ChangePercentageDiscount.new(order_id: order_id, amount: 100) - ) + assert_events_contain( + stream, + PercentageDiscountChanged.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 100, + } + ) + ) do + run_command( + Pricing::ChangePercentageDiscount.new( + order_id: order_id, + amount: 100 + ) + ) + end end end @@ -391,31 +376,40 @@ def test_removing_discount_possible_when_discount_has_been_set_and_then_changed add_item(order_id, product_1_id) stream = stream_name(order_id) run_command( - Pricing::SetPercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT,amount: 10) + Pricing::SetPercentageDiscount.new( + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT, + amount: 10 + ) ) run_command( - Pricing::ChangePercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT, amount: 20) + Pricing::ChangePercentageDiscount.new( + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT, + amount: 20 + ) ) - assert_events_contain( - stream, - PercentageDiscountRemoved.new( - data: { - order_id: order_id, - type: Discounts::GENERAL_DISCOUNT - } - ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 20, - total_amount: 20 - } - ) + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 20, total_amount: 20 } ) do - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT) - ) + assert_events_contain( + stream, + PercentageDiscountRemoved.new( + data: { + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT, + } + ) + ) do + run_command( + Pricing::RemovePercentageDiscount.new( + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT + ) + ) + end end end @@ -425,20 +419,14 @@ def test_removing_with_missing_discount_not_possible order_id = SecureRandom.uuid add_item(order_id, product_1_id) assert_raises NotPossibleToRemoveWithoutDiscount do - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id) - ) + run_command(Pricing::RemovePercentageDiscount.new(order_id: order_id)) end run_command( Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) ) - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id) - ) + run_command(Pricing::RemovePercentageDiscount.new(order_id: order_id)) assert_raises NotPossibleToRemoveWithoutDiscount do - run_command( - Pricing::RemovePercentageDiscount.new(order_id: order_id) - ) + run_command(Pricing::RemovePercentageDiscount.new(order_id: order_id)) end end diff --git a/ecommerce/pricing/test/simple_offer_test.rb b/ecommerce/pricing/test/simple_offer_test.rb index 72a7dac8..4b5098a5 100644 --- a/ecommerce/pricing/test/simple_offer_test.rb +++ b/ecommerce/pricing/test/simple_offer_test.rb @@ -21,13 +21,6 @@ def test_adding total_value: 20, } ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 20, - total_amount: 20 - } - ), PriceItemValueCalculated.new( data: { order_id: order_id, @@ -51,13 +44,6 @@ def test_adding total_value: 40, } ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 40, - total_amount: 40 - } - ), PriceItemValueCalculated.new( data: { order_id: order_id, @@ -91,13 +77,6 @@ def test_removing total_value: 20, } ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 20, - total_amount: 20 - } - ), PriceItemValueCalculated.new( data: { order_id: order_id, @@ -121,13 +100,6 @@ def test_removing total_value: 0, } ), - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - discounted_amount: 0, - total_amount: 0 - } - ) ) { remove_item(order_id, product_id) } end end diff --git a/ecommerce/pricing/test/time_promotion_test.rb b/ecommerce/pricing/test/time_promotion_test.rb index c07a7d6f..a6cd5ab8 100644 --- a/ecommerce/pricing/test/time_promotion_test.rb +++ b/ecommerce/pricing/test/time_promotion_test.rb @@ -52,26 +52,24 @@ def test_calculates_total_value_with_time_promotion run_command(SetTimePromotionDiscount.new(order_id: order_id, amount: 50)) - 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, - total_amount: 20, - discounted_amount: 10 - } - ) - ) { add_item(order_id, product_1_id) } + assert_published_within( + OrderTotalValueCalculated, + { order_id: order_id, discounted_amount: 10, total_amount: 20 } + ) do + 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 + } + ) + ) { add_item(order_id, product_1_id) } + end end def test_calculates_sub_amounts_with_combined_discounts @@ -200,9 +198,20 @@ def stream_name(order_id) "Pricing::Offer$#{order_id}" end - def set_time_promotion_range(time_promotion_id, start_time, end_time, discount) + def set_time_promotion_range( + time_promotion_id, + start_time, + end_time, + discount + ) run_command( - CreateTimePromotion.new(time_promotion_id: time_promotion_id, start_time: start_time, end_time: end_time, discount: discount, label: "test") + CreateTimePromotion.new( + time_promotion_id: time_promotion_id, + start_time: start_time, + end_time: end_time, + discount: discount, + label: "test" + ) ) end diff --git a/infra/lib/infra/testing.rb b/infra/lib/infra/testing.rb index 92bb521b..f6b6d685 100644 --- a/infra/lib/infra/testing.rb +++ b/infra/lib/infra/testing.rb @@ -67,6 +67,34 @@ def assert_no_events(stream_name) before.nil? ? scope.to_a : scope.from(before.event_id).to_a assert_empty actual_events end + + def assert_published_within( + event_type, + event_data, + event_store: @event_store, + &block + ) + before = event_store.read.to_a + block.call + events = + ( + if before.any? + event_store + .read + .newer_than(before.last.timestamp) + .of_type(event_type) + .to_a + else + event_store.read.of_type(event_type).to_a + end + ) + refute events.empty?, "expected some events, none were there" + events.each do |e| + assert_equal event_type.to_s, e.event_type + assert_equal event_data.with_indifferent_access, + e.data.with_indifferent_access + end + end end end