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/discounts.rb b/ecommerce/pricing/lib/pricing/discounts.rb index e72c4976..3c18b604 100644 --- a/ecommerce/pricing/lib/pricing/discounts.rb +++ b/ecommerce/pricing/lib/pricing/discounts.rb @@ -60,5 +60,15 @@ def add(other_discount) def exists? end end + + class ThreePlusOneGratis + def apply(product_quantities, product_id, base_price) + 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 end diff --git a/ecommerce/pricing/lib/pricing/events.rb b/ecommerce/pricing/lib/pricing/events.rb index d5d52a75..89e17c66 100644 --- a/ecommerce/pricing/lib/pricing/events.rb +++ b/ecommerce/pricing/lib/pricing/events.rb @@ -36,18 +36,27 @@ 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 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 + attribute? :applied_promotion, Infra::Types::String 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..e2aba86e 100644 --- a/ecommerce/pricing/lib/pricing/offer.rb +++ b/ecommerce/pricing/lib/pricing/offer.rb @@ -12,23 +12,39 @@ def initialize(id) @state = :draft end - def add_item(product_id, price) - apply PriceItemAdded.new( - data: { - order_id: @id, - product_id: product_id, - price: price, - } - ) + def add_item(product_id, base_price, promotion) + if promotion + 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 + + 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_applies + + apply PriceItemAdded.new(data:) 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 @@ -39,7 +55,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 @@ -87,7 +105,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 +128,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 +174,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 +189,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 +231,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 +246,34 @@ def remove_item(product_id, price) @items.delete_at(index_of_item_to_remove) end + 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 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 @@ -253,15 +289,14 @@ 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(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/lib/pricing/services.rb b/ecommerce/pricing/lib/pricing/services.rb index 4744fcc0..ed5d7d86 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 = Discounts::ThreePlusOneGratis.new if command.promotion + order.add_item(command.product_id, command.price, promotion) end end end diff --git a/ecommerce/pricing/test/apply_time_promotion_test.rb b/ecommerce/pricing/test/apply_time_promotion_test.rb index d8e2678d..7edf1674 100644 --- a/ecommerce/pricing/test/apply_time_promotion_test.rb +++ b/ecommerce/pricing/test/apply_time_promotion_test.rb @@ -5,6 +5,7 @@ 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) @@ -57,13 +58,18 @@ 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, - price: 1111, + base_price: 1000, + price: 1000, + base_total_value: 1000, + 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) @@ -75,7 +81,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/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/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 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( 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 diff --git a/ecommerce/pricing/test/test_helper.rb b/ecommerce/pricing/test/test_helper.rb index 4830dd82..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 @@ -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 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..e96ace03 --- /dev/null +++ b/ecommerce/pricing/test/three_plus_one_test.rb @@ -0,0 +1,574 @@ +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: 0, + base_total_value: 80, + total_value: 60, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s + } + ), + 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, promotion: true) } + 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 + } + ), + 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, + applied_promotion: Pricing::Discounts::ThreePlusOneGratis.to_s + } + ), + 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, promotion: true) } + assert_events( + stream, + PercentageDiscountSet.new( + data: { + order_id: order_id, + type: Discounts::GENERAL_DISCOUNT, + amount: 10, + base_total_value: 80, + total_value: 54 + } + ), + OrderTotalValueCalculated.new( + data: { + order_id: order_id, + 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 + + 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 + + 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 + + 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 diff --git a/ecommerce/pricing/test/time_promotion_test.rb b/ecommerce/pricing/test/time_promotion_test.rb index a3ccd999..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 @@ -76,7 +85,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" } 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 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..75c1cdea 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, } ) ) @@ -218,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 } ) ) @@ -230,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 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