From d32c4f1a331f7739b67e3a0d2c1d9cc2ac40936b Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Sat, 3 May 2025 21:47:05 +0200 Subject: [PATCH 1/2] Introduce new assertion --- infra/lib/infra/testing.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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 From b9a990f514d5e967952aa0ed4777b977730b0671 Mon Sep 17 00:00:00 2001 From: lukaszreszke Date: Sat, 3 May 2025 21:48:16 +0200 Subject: [PATCH 2/2] Publish OrderTotalValueCalculated from CalculateTotalValue Instead of publishing this event from the aggregate now we're going to publish it from the event handler. This is temporary. Ideally we'd have an calculator for that that would subscribe to all the events that affect the offer. Additionally we'll get benefits from both of suggested solutions up until now: - we'll remove calculate_order_total_value from aggregate in next commits - read models are going to subscribe to whatever is published from CalculateTotalValue and won't have to calculate anything on their own - in next steps the events such as PriceItemAdded, PriceItemRemoved will get rid of base_total_value and total_value - change is going to be simpler as the event stands still, it is just going to be produced by different component. One that will be responsible for doing calculations. This way we'll make Offer care only about items and promotions for the offer, not the price itself. --- .../pricing/calculate_order_total_value.rb | 77 ++++- ecommerce/pricing/test/free_products_test.rb | 205 ++++++++----- ecommerce/pricing/test/pricing_test.rb | 290 +++++++++--------- ecommerce/pricing/test/simple_offer_test.rb | 28 -- ecommerce/pricing/test/time_promotion_test.rb | 53 ++-- 5 files changed, 364 insertions(+), 289 deletions(-) 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