diff --git a/ecommerce/pricing/lib/pricing.rb b/ecommerce/pricing/lib/pricing.rb index 64b5735e..adb28218 100644 --- a/ecommerce/pricing/lib/pricing.rb +++ b/ecommerce/pricing/lib/pricing.rb @@ -11,6 +11,7 @@ require_relative "pricing/promotions_calendar" require_relative "pricing/calculate_order_sub_amounts_value" require_relative "pricing/calculate_order_total_value" +require_relative "pricing/apply_time_promotion" module Pricing def self.command_bus=(value) @@ -90,6 +91,23 @@ def call(event_store, command_bus) UseCoupon, UseCouponHandler.new(event_store) ) + command_bus.register( + SetTimePromotionDiscount, + SetTimePromotionDiscountHandler.new(event_store) + ) + command_bus.register( + ResetTimePromotionDiscount, + ResetTimePromotionDiscountHandler.new(event_store) + ) + event_store.subscribe(ApplyTimePromotion, to: [ + PriceItemAdded, + PriceItemRemoved, + PercentageDiscountSet, + PercentageDiscountReset, + PercentageDiscountChanged, + ProductMadeFreeForOrder, + FreeProductRemovedFromOrder + ]) event_store.subscribe(CalculateOrderTotalValue, to: [ PriceItemAdded, PriceItemRemoved, diff --git a/ecommerce/pricing/lib/pricing/apply_time_promotion.rb b/ecommerce/pricing/lib/pricing/apply_time_promotion.rb new file mode 100644 index 00000000..3281919c --- /dev/null +++ b/ecommerce/pricing/lib/pricing/apply_time_promotion.rb @@ -0,0 +1,25 @@ +module Pricing + class ApplyTimePromotion + def call(event) + discount = PromotionsCalendar.new(event_store).current_time_promotions_discount + + if discount.exists? + command_bus.(SetTimePromotionDiscount.new(order_id: event.data.fetch(:order_id), amount: discount.value)) + else + command_bus.(ResetTimePromotionDiscount.new(order_id: event.data.fetch(:order_id))) + end + + rescue NotPossibleToAssignDiscountTwice, NotPossibleToResetWithoutDiscount + end + + private + + def command_bus + Pricing.command_bus + end + + def event_store + Pricing.event_store + end + end +end diff --git a/ecommerce/pricing/lib/pricing/commands.rb b/ecommerce/pricing/lib/pricing/commands.rb index 94bb2596..832f9473 100644 --- a/ecommerce/pricing/lib/pricing/commands.rb +++ b/ecommerce/pricing/lib/pricing/commands.rb @@ -45,6 +45,19 @@ class ResetPercentageDiscount < Infra::Command alias aggregate_id order_id end + class SetTimePromotionDiscount < Infra::Command + attribute :order_id, Infra::Types::UUID + attribute :amount, Infra::Types::PercentageDiscount + + alias aggregate_id order_id + end + + class ResetTimePromotionDiscount < Infra::Command + attribute :order_id, Infra::Types::UUID + + alias aggregate_id order_id + end + class RegisterCoupon < Infra::Command attribute :coupon_id, Infra::Types::UUID attribute :name, Infra::Types::String diff --git a/ecommerce/pricing/lib/pricing/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index 8134c7ed..97e99cfb 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -1,5 +1,8 @@ module Pricing module Discounts + GENERAL_DISCOUNT = "general_discount" + TIME_PROMOTION_DISCOUNT = "time_promotion_discount" + class UnacceptableDiscountRange < StandardError end @@ -53,10 +56,6 @@ def add(other_discount) other_discount end - def value - 0 - end - def exists? end end diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index 79120e3f..a985f765 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -34,7 +34,8 @@ class PriceItemValueCalculated < Infra::Event class PercentageDiscountSet < Infra::Event attribute :order_id, Infra::Types::UUID - attribute :amount, Infra::Types::Price + attribute :type, Infra::Types::String + attribute :amount, Infra::Types::PercentageDiscount end class PriceItemAdded < Infra::Event @@ -49,10 +50,12 @@ class PriceItemRemoved < Infra::Event class PercentageDiscountReset < Infra::Event attribute :order_id, Infra::Types::UUID + attribute :type, Infra::Types::String end class PercentageDiscountChanged < Infra::Event attribute :order_id, Infra::Types::UUID + attribute :type, Infra::Types::String attribute :amount, Infra::Types::Price end diff --git a/ecommerce/pricing/lib/pricing/offer.rb b/ecommerce/pricing/lib/pricing/offer.rb index 34675493..dca2f3da 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -5,7 +5,7 @@ class Offer def initialize(id) @id = id @list = List.new - @discount = Discounts::NoPercentageDiscount.new + @discounts = {} end def add_item(product_id) @@ -26,31 +26,34 @@ def remove_item(product_id) ) end - def apply_discount(discount) - raise NotPossibleToAssignDiscountTwice if @discount.exists? + def apply_discount(type, discount) + raise NotPossibleToAssignDiscountTwice if @discounts.include?(type) apply PercentageDiscountSet.new( data: { order_id: @id, + type: type, amount: discount.value } ) end - def change_discount(discount) - raise NotPossibleToChangeDiscount unless @discount.exists? + def change_discount(type, discount) + raise NotPossibleToChangeDiscount unless @discounts.include?(type) apply PercentageDiscountChanged.new( data: { order_id: @id, + type: type, amount: discount.value } ) end - def reset_discount - raise NotPossibleToResetWithoutDiscount unless @discount.exists? + def reset_discount(type) + raise NotPossibleToResetWithoutDiscount unless @discounts.include?(type) apply PercentageDiscountReset.new( data: { - order_id: @id + order_id: @id, + type: type } ) end @@ -75,10 +78,10 @@ def remove_free_product(order_id, product_id) ) end - def calculate_total_value(pricing_catalog, time_promotion_discount) + def calculate_total_value(pricing_catalog) total_value = @list.base_sum(pricing_catalog) + discounted_value = @discounts.values.inject(Discounts::NoPercentageDiscount.new, :add).apply(total_value) - discounted_value = @discount.add(time_promotion_discount).apply(total_value) apply( OrderTotalValueCalculated.new( data: { @@ -90,9 +93,9 @@ def calculate_total_value(pricing_catalog, time_promotion_discount) ) end - def calculate_sub_amounts(pricing_catalog, time_promotions_discount) + def calculate_sub_amounts(pricing_catalog) sub_amounts_total = @list.sub_amounts_total(pricing_catalog) - sub_discounts = calculate_total_sub_discounts(pricing_catalog, time_promotions_discount) + sub_discounts = calculate_total_sub_discounts(pricing_catalog) products = @list.products quantities = @list.quantities @@ -138,15 +141,15 @@ def use_coupon(coupon_id, discount) end on PercentageDiscountSet do |event| - @discount = Discounts::PercentageDiscount.new(event.data.fetch(:amount)) + @discounts[event.data.fetch(:type)] = Discounts::PercentageDiscount.new(event.data.fetch(:amount)) end on PercentageDiscountChanged do |event| - @discount = Discounts::PercentageDiscount.new(event.data.fetch(:amount)) + @discounts[event.data.fetch(:type)] = Discounts::PercentageDiscount.new(event.data.fetch(:amount)) end on PercentageDiscountReset do |event| - @discount = Discounts::NoPercentageDiscount.new + @discounts.delete(event.data.fetch(:type)) end on ProductMadeFreeForOrder do |event| @@ -157,8 +160,8 @@ def use_coupon(coupon_id, discount) @list.replace(FreeProduct, Product, event.data.fetch(:product_id)) end - def calculate_total_sub_discounts(pricing_catalog, time_promotions_discount) - @list.sub_discounts(pricing_catalog, time_promotions_discount, @discount) + def calculate_total_sub_discounts(pricing_catalog) + @list.sub_discounts(pricing_catalog, @discounts) end on CouponUsed do |event| @@ -209,10 +212,10 @@ def sub_amounts_total(pricing_catalog) @products_quantities.map { |product, quantity| quantity * pricing_catalog.price_for(product) } end - def sub_discounts(pricing_catalog, time_promotions_discount, discount) + def sub_discounts(pricing_catalog, discounts) @products_quantities.map do |product, quantity| catalog_price_for_single = pricing_catalog.price_for(product) - with_total_discount_single = discount.add(time_promotions_discount).apply(catalog_price_for_single) + with_total_discount_single = discounts.values.inject(Discounts::NoPercentageDiscount.new, :add).apply(catalog_price_for_single) quantity * (catalog_price_for_single - with_total_discount_single) end end diff --git a/ecommerce/pricing/lib/pricing/services.rb b/ecommerce/pricing/lib/pricing/services.rb index bfd29ebb..18f744b2 100644 --- a/ecommerce/pricing/lib/pricing/services.rb +++ b/ecommerce/pricing/lib/pricing/services.rb @@ -21,7 +21,7 @@ def initialize(event_store) def call(cmd) @repository.with_aggregate(Offer, cmd.aggregate_id) do |order| - order.apply_discount(Discounts::PercentageDiscount.new(cmd.amount)) + order.apply_discount(Discounts::GENERAL_DISCOUNT, Discounts::PercentageDiscount.new(cmd.amount)) end end end @@ -33,7 +33,7 @@ def initialize(event_store) def call(cmd) @repository.with_aggregate(Offer, cmd.aggregate_id) do |order| - order.reset_discount + order.reset_discount(Discounts::GENERAL_DISCOUNT) end end end @@ -45,7 +45,31 @@ def initialize(event_store) def call(cmd) @repository.with_aggregate(Offer, cmd.aggregate_id) do |order| - order.change_discount(Discounts::PercentageDiscount.new(cmd.amount)) + order.change_discount(Discounts::GENERAL_DISCOUNT, Discounts::PercentageDiscount.new(cmd.amount)) + end + end + end + + class SetTimePromotionDiscountHandler + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(cmd) + @repository.with_aggregate(Offer, cmd.aggregate_id) do |order| + order.apply_discount(Discounts::TIME_PROMOTION_DISCOUNT, Discounts::PercentageDiscount.new(cmd.amount)) + end + end + end + + class ResetTimePromotionDiscountHandler + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(cmd) + @repository.with_aggregate(Offer, cmd.aggregate_id) do |order| + order.reset_discount(Discounts::TIME_PROMOTION_DISCOUNT) end end end @@ -124,27 +148,18 @@ def initialize(event_store) def call(command) with_retry do @repository.with_aggregate(Offer, command.aggregate_id) do |order| - order.calculate_total_value(PricingCatalog.new(@event_store), time_promotions_discount) + order.calculate_total_value(PricingCatalog.new(@event_store)) end end end - - def calculate_sub_amounts(command) with_retry do @repository.with_aggregate(Offer, command.aggregate_id) do |order| - order.calculate_sub_amounts(PricingCatalog.new(@event_store), time_promotions_discount) + order.calculate_sub_amounts(PricingCatalog.new(@event_store)) end end end - - private - - def time_promotions_discount - PromotionsCalendar.new(@event_store).current_time_promotions_discount - end - end class OnCouponRegister diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb new file mode 100644 index 00000000..b8dbeaea --- /dev/null +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -0,0 +1,95 @@ +require_relative "test_helper" + +module Pricing + class ApplyTimePromotionTest < Test + cover "Pricing*" + + def test_applies_biggest_time_promotion_discount + order_id = SecureRandom.uuid + product_id = SecureRandom.uuid + create_inactive_time_promotion(60) + create_active_time_promotion(10) + create_active_time_promotion(50) + create_active_time_promotion(30) + + assert_events_contain(stream_name(order_id), percentage_discount_set_event(order_id, 50)) do + Pricing::ApplyTimePromotion.new.call(item_added_to_basket_event(order_id, product_id)) + end + end + + def test_resets_time_promotion_discount + order_id = SecureRandom.uuid + product_id = SecureRandom.uuid + create_active_time_promotion(50) + set_time_promotion_discount(order_id, 50) + + Timecop.travel(1.minute.from_now) do + assert_events_contain(stream_name(order_id), percentage_discount_reset_event(order_id)) do + Pricing::ApplyTimePromotion.new.call(item_added_to_basket_event(order_id, product_id)) + end + end + end + + private + + def create_inactive_time_promotion(discount) + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: discount, + start_time: Time.current - 2, + end_time: Time.current - 1, + label: "Past Promotion" + ) + ) + end + + def create_active_time_promotion(discount) + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: discount, + start_time: Time.current - 1, + end_time: Time.current + 1, + label: "Last Minute" + ) + ) + end + + def item_added_to_basket_event(order_id, product_id) + Pricing::PriceItemAdded.new( + data: { + product_id: product_id, + order_id: order_id + } + ) + end + + def set_time_promotion_discount(order_id, discount) + run_command(SetTimePromotionDiscount.new(order_id: order_id, amount: discount)) + end + + def percentage_discount_set_event(order_id, amount) + PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Pricing::Discounts::TIME_PROMOTION_DISCOUNT, + amount: amount + } + ) + end + + def percentage_discount_reset_event(order_id) + PercentageDiscountReset.new( + data: { + order_id: order_id, + type: Pricing::Discounts::TIME_PROMOTION_DISCOUNT + } + ) + end + + def stream_name(order_id) + "Pricing::Offer$#{order_id}" + end + end +end diff --git a/ecommerce/pricing/test/pricing_test.rb b/ecommerce/pricing/test/pricing_test.rb index 14f2326e..2067b5a9 100644 --- a/ecommerce/pricing/test/pricing_test.rb +++ b/ecommerce/pricing/test/pricing_test.rb @@ -93,6 +93,53 @@ def test_calculates_sub_amounts ) { calculate_sub_amounts(order_id) } end + def test_sets_time_promotion_discount + order_id = SecureRandom.uuid + stream = stream_name(order_id) + + assert_events_contain( + stream, + Pricing::PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Discounts::TIME_PROMOTION_DISCOUNT, + amount: 25 + } + ) + ) { set_time_promotion_discount(order_id, 25) } + end + + def test_does_not_set_the_same_time_promotion_discount_twice + order_id = SecureRandom.uuid + create_active_time_promotion(25) + set_time_promotion_discount(order_id, 25) + + assert_raises(NotPossibleToAssignDiscountTwice) { set_time_promotion_discount(order_id, 25) } + end + + def test_resets_time_promotion_discount + order_id = SecureRandom.uuid + stream = stream_name(order_id) + create_active_time_promotion(25) + set_time_promotion_discount(order_id, 25) + + assert_events_contain( + stream, + PercentageDiscountReset.new( + data: { + order_id: order_id, + type: Discounts::TIME_PROMOTION_DISCOUNT + } + ) + ) { reset_time_promotion_discount(order_id) } + end + + def test_does_not_reset_time_promotion_discount_if_there_is_none + order_id = SecureRandom.uuid + + assert_raises(NotPossibleToResetWithoutDiscount) { reset_time_promotion_discount(order_id) } + end + def test_calculates_total_value_with_discount product_1_id = SecureRandom.uuid set_price(product_1_id, 20) @@ -114,6 +161,7 @@ def test_calculates_total_value_with_discount PercentageDiscountSet.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 10 } ), @@ -126,7 +174,7 @@ def test_calculates_total_value_with_discount ) ) do run_command( - Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) + Pricing::SetPercentageDiscount.new(order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 10) ) end assert_events_contain( @@ -134,6 +182,7 @@ def test_calculates_total_value_with_discount PercentageDiscountChanged.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 50 } ), @@ -154,6 +203,7 @@ def test_calculates_total_value_with_discount PercentageDiscountReset.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT } ), OrderTotalValueCalculated.new( @@ -165,7 +215,7 @@ def test_calculates_total_value_with_discount ) ) do run_command( - Pricing::ResetPercentageDiscount.new(order_id: order_id) + Pricing::ResetPercentageDiscount.new(order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT) ) end end @@ -181,6 +231,7 @@ def test_calculates_total_value_with_100_discount PercentageDiscountSet.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 100 } ), @@ -279,6 +330,7 @@ def test_changing_discount_possible_when_discount_is_set PercentageDiscountChanged.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 100 } ), @@ -314,6 +366,7 @@ def test_changing_discount_possible_more_than_once PercentageDiscountChanged.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 100 } ), @@ -338,17 +391,18 @@ def test_resetting_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, amount: 10) + Pricing::SetPercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT,amount: 10) ) run_command( - Pricing::ChangePercentageDiscount.new(order_id: order_id, amount: 20) + Pricing::ChangePercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT, amount: 20) ) assert_events_contain( stream, PercentageDiscountReset.new( data: { - order_id: order_id + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT } ), OrderTotalValueCalculated.new( @@ -360,7 +414,7 @@ def test_resetting_discount_possible_when_discount_has_been_set_and_then_changed ) ) do run_command( - Pricing::ResetPercentageDiscount.new(order_id: order_id) + Pricing::ResetPercentageDiscount.new(order_id: order_id, type: Discounts::GENERAL_DISCOUNT) ) end end @@ -397,5 +451,17 @@ def stream_name(order_id) def calculate_sub_amounts(order_id) run_command(CalculateSubAmounts.new(order_id: order_id)) end + + def create_active_time_promotion(discount) + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: discount, + start_time: Time.current - 1.minute, + end_time: Time.current + 1.minute, + label: "Last Minute" + ) + ) + end end end diff --git a/ecommerce/pricing/test/promotions_calendar_test.rb b/ecommerce/pricing/test/promotions_calendar_test.rb new file mode 100644 index 00000000..b5829804 --- /dev/null +++ b/ecommerce/pricing/test/promotions_calendar_test.rb @@ -0,0 +1,30 @@ +require_relative "test_helper" + +module Pricing + class PromotionsCalendarTest < Test + cover "Pricing::PromotionsCalendar*" + + def test_time_promotion_running + time_current = Time.current + promotion = Pricing::PromotionsCalendar::Promotion.from_event(time_promotion_created_event(time_current)) + + assert(promotion.running?(time_current)) + refute(promotion.running?(time_current + 1)) + refute(promotion.running?(time_current - 2)) + refute(promotion.running?(time_current + 2)) + end + + private + + def time_promotion_created_event(time_current) + TimePromotionCreated.new( + data: { + time_promotion_id: SecureRandom.uuid, + start_time: time_current - 1, + end_time: time_current + 1, + discount: 10 + } + ) + end + end +end diff --git a/ecommerce/pricing/test/test_helper.rb b/ecommerce/pricing/test/test_helper.rb index eb9e385f..66b1c8b5 100644 --- a/ecommerce/pricing/test/test_helper.rb +++ b/ecommerce/pricing/test/test_helper.rb @@ -42,6 +42,14 @@ def register_coupon(uid, name, code, discount) run_command(RegisterCoupon.new(coupon_id: uid, name: name, code: code, discount: discount)) end + def set_time_promotion_discount(order_id, amount) + run_command(SetTimePromotionDiscount.new(order_id: order_id, amount: amount)) + end + + def reset_time_promotion_discount(order_id) + run_command(ResetTimePromotionDiscount.new(order_id: order_id)) + end + def fake_name "Fake name" end diff --git a/ecommerce/pricing/test/time_promotion_test.rb b/ecommerce/pricing/test/time_promotion_test.rb index 2399bf1b..a3ccd999 100644 --- a/ecommerce/pricing/test/time_promotion_test.rb +++ b/ecommerce/pricing/test/time_promotion_test.rb @@ -41,127 +41,76 @@ class DiscountWithTimePromotionTest < Test cover "Pricing*" def test_calculates_total_value_with_time_promotion - timestamp = Time.utc(2022, 5, 30, 15, 33) - - Timecop.freeze(timestamp) do - product_1_id = SecureRandom.uuid - set_price(product_1_id, 20) - 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 - } - ) - ) { calculate_total_value(order_id) } - - # Current promotions - first_time_promotion_id = SecureRandom.uuid - start_time = timestamp - 1 - end_time = timestamp + 1 - set_time_promotion_range(first_time_promotion_id, start_time, end_time, 40) - - time_promotion_id = SecureRandom.uuid - start_time = timestamp - end_time = timestamp + 1 - set_time_promotion_range(time_promotion_id, start_time, end_time, 1) - - - time_promotion_id = SecureRandom.uuid - start_time = timestamp - end_time = timestamp + 1 - set_time_promotion_range(time_promotion_id, start_time, end_time, 50) - - # Not applicable promotions - set_not_applicable_promotions(timestamp) - - assert_events( - stream, - OrderTotalValueCalculated.new( - data: { - order_id: order_id, - total_amount: 20, - discounted_amount: 10, - } - ) - ) { calculate_total_value(order_id) } - end + 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 + end_time = Time.current + 1 + set_time_promotion_range(time_promotion_id, start_time, end_time, 50) + + run_command(SetTimePromotionDiscount.new(order_id: order_id, amount: 50)) + + assert_events_contain( + stream, + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + total_amount: 20, + discounted_amount: 10 + } + ) + ) { calculate_total_value(order_id) } end def test_calculates_sub_amounts_with_combined_discounts - timestamp = Time.utc(2022, 5, 30, 15, 33) - Timecop.freeze(timestamp) do - - product_1_id = SecureRandom.uuid - product_2_id = SecureRandom.uuid - set_price(product_1_id, 20) - set_price(product_2_id, 30) - order_id = SecureRandom.uuid - stream = stream_name(order_id) + product_1_id = SecureRandom.uuid + product_2_id = SecureRandom.uuid + set_price(product_1_id, 20) + set_price(product_2_id, 30) + order_id = SecureRandom.uuid + stream = stream_name(order_id) + data = { + time_promotion_id: SecureRandom.uuid, + discount: 50, + start_time: Time.current - 1, + end_time: Time.current + 1, + label: "Last Minute" + } - assert_events(stream) { calculate_sub_amounts(order_id) } + run_command(CreateTimePromotion.new(data)) - add_item(order_id, product_1_id) - add_item(order_id, product_2_id) - add_item(order_id, product_2_id) - assert_events( - stream, - PriceItemValueCalculated.new( - data: { - order_id: order_id, - product_id: product_1_id, - quantity: 1, - amount: 20, - discounted_amount: 20 - } - ), - PriceItemValueCalculated.new( - data: { - order_id: order_id, - product_id: product_2_id, - quantity: 2, - amount: 60, - discounted_amount: 60 - } - ) - ) { calculate_sub_amounts(order_id) } - run_command( - Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) - ) + add_item(order_id, product_1_id) + add_item(order_id, product_2_id) + add_item(order_id, product_2_id) - first_time_promotion_id = SecureRandom.uuid - start_time = timestamp - 1 - end_time = timestamp + 1 - set_time_promotion_range(first_time_promotion_id, start_time, end_time, 50) + run_command( + Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) + ) - assert_events( - stream, - PriceItemValueCalculated.new( - data: { - order_id: order_id, - product_id: product_1_id, - quantity: 1, - amount: 20, - discounted_amount: 8 - } - ), - PriceItemValueCalculated.new( - data: { - order_id: order_id, - product_id: product_2_id, - quantity: 2, - amount: 60, - discounted_amount: 24 - } - ) - ) { calculate_sub_amounts(order_id) } - end + assert_events_contain( + stream, + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_1_id, + quantity: 1, + amount: 20, + discounted_amount: 8 + } + ), + PriceItemValueCalculated.new( + data: { + order_id: order_id, + product_id: product_2_id, + quantity: 2, + amount: 60, + discounted_amount: 24 + } + ) + ) { calculate_sub_amounts(order_id) } end def test_cant_create_twice diff --git a/rails_application/app/read_models/orders/configuration.rb b/rails_application/app/read_models/orders/configuration.rb index a4bd8ed2..fb09677a 100644 --- a/rails_application/app/read_models/orders/configuration.rb +++ b/rails_application/app/read_models/orders/configuration.rb @@ -46,7 +46,8 @@ def call(event_store) event_store.subscribe(ExpireOrder.new, to: [Ordering::OrderExpired]) event_store.subscribe(ConfirmOrder.new, to: [Fulfillment::OrderConfirmed]) event_store.subscribe(CancelOrder.new, to: [Fulfillment::OrderCancelled]) - + event_store.subscribe(UpdateTimePromotionDiscountValue.new, to: [Pricing::PercentageDiscountSet]) + event_store.subscribe(ResetTimePromotionDiscountValue.new, to: [Pricing::PercentageDiscountReset]) subscribe( ->(event) { broadcast_order_state_change(event.data.fetch(:order_id), 'Submitted') }, diff --git a/rails_application/app/read_models/orders/reset_discount.rb b/rails_application/app/read_models/orders/reset_discount.rb index 8c637576..42037348 100644 --- a/rails_application/app/read_models/orders/reset_discount.rb +++ b/rails_application/app/read_models/orders/reset_discount.rb @@ -1,6 +1,8 @@ module Orders class ResetDiscount def call(event) + return unless event.data.fetch(:type) == Pricing::Discounts::GENERAL_DISCOUNT + order = Order.find_by_uid(event.data.fetch(:order_id)) order.percentage_discount = nil order.save! diff --git a/rails_application/app/read_models/orders/reset_time_promotion_discount_value.rb b/rails_application/app/read_models/orders/reset_time_promotion_discount_value.rb new file mode 100644 index 00000000..c4741ab5 --- /dev/null +++ b/rails_application/app/read_models/orders/reset_time_promotion_discount_value.rb @@ -0,0 +1,12 @@ +module Orders + class ResetTimePromotionDiscountValue + def call(event) + return unless event.data.fetch(:type) == Pricing::Discounts::TIME_PROMOTION_DISCOUNT + + order = Order.find_by(uid: event.data.fetch(:order_id)) + + order.time_promotion_discount_value = nil + order.save! + end + end +end diff --git a/rails_application/app/read_models/orders/update_discount.rb b/rails_application/app/read_models/orders/update_discount.rb index cf55f2e2..71030819 100644 --- a/rails_application/app/read_models/orders/update_discount.rb +++ b/rails_application/app/read_models/orders/update_discount.rb @@ -1,6 +1,8 @@ module Orders class UpdateDiscount def call(event) + return unless event.data.fetch(:type) == Pricing::Discounts::GENERAL_DISCOUNT + order = Order.find_or_create_by(uid: event.data.fetch(:order_id)) if is_newest_value?(event, order) order.percentage_discount = event.data.fetch(:amount) diff --git a/rails_application/app/read_models/orders/update_time_promotion_discount_value.rb b/rails_application/app/read_models/orders/update_time_promotion_discount_value.rb new file mode 100644 index 00000000..aebf0ea2 --- /dev/null +++ b/rails_application/app/read_models/orders/update_time_promotion_discount_value.rb @@ -0,0 +1,12 @@ +module Orders + class UpdateTimePromotionDiscountValue + def call(event) + return unless event.data.fetch(:type) == Pricing::Discounts::TIME_PROMOTION_DISCOUNT + + order = Order.find_or_create_by(uid: event.data.fetch(:order_id)) + + order.time_promotion_discount_value = event.data.fetch(:amount) + order.save! + end + end +end diff --git a/rails_application/app/views/orders/show.html.erb b/rails_application/app/views/orders/show.html.erb index dc269b15..cf0ccd3b 100644 --- a/rails_application/app/views/orders/show.html.erb +++ b/rails_application/app/views/orders/show.html.erb @@ -104,6 +104,12 @@ <%= @order.percentage_discount %>% <% end %> + <% if @order.time_promotion_discount_value %> + + Time Promotion discount + <%= @order.time_promotion_discount_value %>% + + <% end %> Total diff --git a/rails_application/db/migrate/20241002130622_rename_happy_hour_value_to_time_promotion_discount_value.rb b/rails_application/db/migrate/20241002130622_rename_happy_hour_value_to_time_promotion_discount_value.rb new file mode 100644 index 00000000..a9601df8 --- /dev/null +++ b/rails_application/db/migrate/20241002130622_rename_happy_hour_value_to_time_promotion_discount_value.rb @@ -0,0 +1,5 @@ +class RenameHappyHourValueToTimePromotionDiscountValue < ActiveRecord::Migration[7.2] + def change + rename_column :orders, :happy_hour_value, :time_promotion_discount_value + end +end diff --git a/rails_application/db/schema.rb b/rails_application/db/schema.rb index aa2604e1..939d0106 100644 --- a/rails_application/db/schema.rb +++ b/rails_application/db/schema.rb @@ -160,7 +160,7 @@ t.decimal "percentage_discount", precision: 8, scale: 2 t.decimal "total_value", precision: 8, scale: 2 t.decimal "discounted_value", precision: 8, scale: 2 - t.decimal "happy_hour_value", precision: 8, scale: 2 + t.decimal "time_promotion_discount_value", precision: 8, scale: 2 t.datetime "total_value_updated_at" t.datetime "discount_updated_at" t.index ["uid"], name: "index_orders_on_uid", unique: true diff --git a/rails_application/test/integration/orders_test.rb b/rails_application/test/integration/orders_test.rb index f7d3b643..d5e211c4 100644 --- a/rails_application/test/integration/orders_test.rb +++ b/rails_application/test/integration/orders_test.rb @@ -250,7 +250,7 @@ def test_current_time_promotion_is_applied async_remote_id = register_product("Async Remote", 39, 10) shopify_id = register_customer("Shopify") - create_current_time_promotion + create_active_time_promotion(50) post "/orders/#{order_id}/add_item?product_id=#{async_remote_id}" post "/orders", @@ -264,6 +264,7 @@ def test_current_time_promotion_is_applied assert_select("td", "$19.50") assert_select("dd", "Submitted") + assert_select("td", "50.0%") end private @@ -377,12 +378,12 @@ def apply_discount_10_percent(order_id) post "/orders/#{order_id}/update_discount?amount=10" end - def create_current_time_promotion(discount: 50, start_time: Time.current - 1.day, end_time: Time.current + 1.day) + def create_active_time_promotion(discount) post "/time_promotions", params: { label: "Last Minute", discount: discount, - start_time: start_time, - end_time: end_time + start_time: Time.current - 1, + end_time: Time.current + 1.minute } end end diff --git a/rails_application/test/orders/broadcast_test.rb b/rails_application/test/orders/broadcast_test.rb index a4a21df4..9d8bb565 100644 --- a/rails_application/test/orders/broadcast_test.rb +++ b/rails_application/test/orders/broadcast_test.rb @@ -210,6 +210,7 @@ def test_broadcast_update_discount Pricing::PercentageDiscountSet.new( data: { order_id: order_1_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 30 } ) @@ -221,6 +222,7 @@ def test_broadcast_update_discount Pricing::PercentageDiscountSet.new( data: { order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, amount: 30 } ) diff --git a/rails_application/test/orders/discount_test.rb b/rails_application/test/orders/discount_test.rb index bd9d9de7..37184b60 100644 --- a/rails_application/test/orders/discount_test.rb +++ b/rails_application/test/orders/discount_test.rb @@ -18,6 +18,7 @@ def test_discount_set assert_equal 50, order.total_value assert_equal 45, order.discounted_value assert_equal 10, order.percentage_discount + assert_nil order.time_promotion_discount_value assert event_store.event_in_stream?(event_store.read.of_type([Pricing::PercentageDiscountSet]).last.event_id, "Orders$all") end @@ -36,6 +37,7 @@ def test_discount_changed assert_equal 50, order.total_value assert_equal 49.5, order.discounted_value assert_equal 1, order.percentage_discount + assert_nil order.time_promotion_discount_value assert event_store.event_in_stream?(event_store.read.of_type([Pricing::PercentageDiscountChanged]).last.event_id, "Orders$all") end @@ -57,6 +59,21 @@ def test_reset_discount assert event_store.event_in_stream?(event_store.read.of_type([Pricing::PercentageDiscountReset]).last.event_id, "Orders$all") end + def test_does_not_reset_percentage_discount_when_time_promotion_reset + customer_id = SecureRandom.uuid + product_id = SecureRandom.uuid + order_id = SecureRandom.uuid + create_active_time_promotion + customer_registered(customer_id) + prepare_product(product_id) + item_added_to_basket(order_id, product_id) + set_percentage_discount(order_id) + + assert_no_changes -> { Orders::Order.find_by(uid: order_id).percentage_discount } do + travel_to(1.minute.from_now) { item_added_to_basket(order_id, product_id) } + end + end + def test_newest_event_is_always_applied customer_id = SecureRandom.uuid product_id = SecureRandom.uuid @@ -65,8 +82,26 @@ def test_newest_event_is_always_applied prepare_product(product_id) item_added_to_basket(order_id, product_id) - event_store.publish(Pricing::PercentageDiscountSet.new(data: { order_id: order_id, amount: 30 }, metadata: { timestamp: Time.current })) - event_store.publish(Pricing::PercentageDiscountSet.new(data: { order_id: order_id, amount: 20 }, metadata: { timestamp: 1.minute.ago })) + event_store.publish(Pricing::PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 30 + }, + metadata: { + timestamp: Time.current + }) + ) + event_store.publish(Pricing::PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Pricing::Discounts::GENERAL_DISCOUNT, + amount: 20 + }, + metadata: { + timestamp: 1.minute.ago + }) + ) assert_equal 30, Orders::Order.find_by(uid: order_id).percentage_discount end @@ -111,6 +146,17 @@ def customer_registered(customer_id) def event_store Rails.configuration.event_store end + + def create_active_time_promotion + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: 50, + start_time: Time.current - 1, + end_time: Time.current + 1, + label: "Last Minute" + ) + ) + end end end - diff --git a/rails_application/test/orders/reset_time_promotion_discount_value_test.rb b/rails_application/test/orders/reset_time_promotion_discount_value_test.rb new file mode 100644 index 00000000..a835282a --- /dev/null +++ b/rails_application/test/orders/reset_time_promotion_discount_value_test.rb @@ -0,0 +1,91 @@ +require "test_helper" + +module Orders + class ResetTimePromotionDiscountValueTest < InMemoryTestCase + cover "Orders*" + + def test_resets_time_promotion_discount_value + customer_id = SecureRandom.uuid + product_id = SecureRandom.uuid + order_id = SecureRandom.uuid + create_active_time_promotion + customer_registered(customer_id) + prepare_product(product_id) + item_added_to_basket(order_id, product_id) + + travel_to(1.minute.from_now) do + item_added_to_basket(order_id, product_id) + order = Orders::Order.find_by(uid: order_id) + assert_nil order.time_promotion_discount_value + end + end + + def test_does_not_reset_time_promotion_when_general_discount_reset + customer_id = SecureRandom.uuid + product_id = SecureRandom.uuid + order_id = SecureRandom.uuid + create_active_time_promotion + customer_registered(customer_id) + prepare_product(product_id) + item_added_to_basket(order_id, product_id) + set_percentage_discount(order_id) + + assert_no_changes -> { Orders::Order.find_by(uid: order_id).time_promotion_discount_value } do + reset_percentage_discount(order_id) + end + end + + 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 })) + end + + def prepare_product(product_id) + run_command( + ProductCatalog::RegisterProduct.new( + product_id: product_id, + ) + ) + run_command( + ProductCatalog::NameProduct.new( + product_id: product_id, + name: "test" + ) + ) + run_command(Pricing::SetPrice.new(product_id: product_id, price: 50)) + end + + def customer_registered(customer_id) + event_store.publish(Crm::CustomerRegistered.new(data: { customer_id: customer_id, name: "Arkency" })) + end + + def create_active_time_promotion + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: 50, + start_time: Time.current - 1, + end_time: Time.current + 1, + label: "Last Minute" + ) + ) + end + + def event_store + Rails.configuration.event_store + end + + def set_percentage_discount(order_id) + run_command( + Pricing::SetPercentageDiscount.new(order_id: order_id, amount: 10) + ) + end + + def reset_percentage_discount(order_id) + run_command( + Pricing::ResetPercentageDiscount.new(order_id: order_id, type: Pricing::Discounts::GENERAL_DISCOUNT) + ) + end + end +end 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 new file mode 100644 index 00000000..b73b92f0 --- /dev/null +++ b/rails_application/test/orders/update_time_promotion_discount_value_test.rb @@ -0,0 +1,62 @@ +require "test_helper" + +module Orders + class UpdateTimePromotionDiscountValueTest < InMemoryTestCase + cover "Orders*" + + def test_updates_time_promotion_discount_value + customer_id = SecureRandom.uuid + product_id = SecureRandom.uuid + order_id = SecureRandom.uuid + create_active_time_promotion + customer_registered(customer_id) + prepare_product(product_id) + item_added_to_basket(order_id, product_id) + + order = Orders::Order.find_by(uid: order_id) + assert_equal 50, order.time_promotion_discount_value + assert_nil order.percentage_discount + end + + 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 })) + end + + def prepare_product(product_id) + run_command( + ProductCatalog::RegisterProduct.new( + product_id: product_id, + ) + ) + run_command( + ProductCatalog::NameProduct.new( + product_id: product_id, + name: "test" + ) + ) + run_command(Pricing::SetPrice.new(product_id: product_id, price: 50)) + end + + def customer_registered(customer_id) + event_store.publish(Crm::CustomerRegistered.new(data: { customer_id: customer_id, name: "Arkency" })) + end + + def create_active_time_promotion + run_command( + Pricing::CreateTimePromotion.new( + time_promotion_id: SecureRandom.uuid, + discount: 50, + start_time: Time.current - 1, + end_time: Time.current + 1, + label: "Last Minute" + ) + ) + end + + def event_store + Rails.configuration.event_store + end + end +end