Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ecommerce/pricing/lib/pricing/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class PriceItemAdded < Infra::Event
class PriceItemRemoved < Infra::Event
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
attribute :price, Infra::Types::Price
end

class PercentageDiscountRemoved < Infra::Event
Expand Down
23 changes: 16 additions & 7 deletions ecommerce/pricing/lib/pricing/offer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ def add_item(product_id, price)
end

def remove_item(product_id)
price = @list.lowest_price(product_id)
apply PriceItemRemoved.new(
data: {
order_id: @id,
product_id: product_id
product_id: product_id,
price: price
}
)
end
Expand Down Expand Up @@ -158,7 +160,7 @@ def expire
end

on PriceItemRemoved do |event|
@list.remove_item(event.data.fetch(:product_id))
@list.remove_item(event.data.fetch(:product_id), event.data.fetch(:price))
end

on PriceItemValueCalculated do |event|
Expand Down Expand Up @@ -220,11 +222,9 @@ def add_item(product_id, price)
@items << Item.new(product_id:, price:, quantity: 1)
end

def remove_item(product_id)
new_items = @items.sort { _1.price}
index_of_item_to_remove = new_items.index { |item| item.product_id == product_id }
new_items.delete_at(index_of_item_to_remove)
@items = new_items
def remove_item(product_id, price)
index_of_item_to_remove = @items.index { |item| item.product_id == product_id && item.price == price }
@items.delete_at(index_of_item_to_remove)
end

def contains_free_products?
Expand All @@ -251,10 +251,19 @@ def set_free(product_id)

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)
end

def lowest_price(product_id)
@items
.select { |item| item.product_id == product_id }
.sort_by(&:price)
.first
&.price
end

def quantities
sub_amounts_total.map do |product_id, h|
{ product_id:, quantity: h.fetch(:quantity) }
Expand Down
38 changes: 7 additions & 31 deletions ecommerce/processes/lib/processes/three_plus_one_free.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ def build_state(event)
end

def make_or_remove_free_product(state)
pricing_catalog = Pricing::PricingCatalog.new(@event_store)
free_product_id = FreeProductResolver.new(state, pricing_catalog).call
free_product_id = state.cheapest_product

return if current_free_product_not_changed?(free_product_id, state)

Expand Down Expand Up @@ -69,51 +68,28 @@ class ProcessState

def initialize(order_id)
@order_id = order_id
@order_lines = Hash.new(0)
@order_lines = []
end

def call(event)
product_id = event.data.fetch(:product_id)
case event
when Pricing::PriceItemAdded
order_lines[product_id] += 1
order_lines << { product_id: event.data.fetch(:product_id), price: event.data.fetch(:price) }
when Pricing::PriceItemRemoved
order_lines[product_id] -= 1
order_lines.delete(product_id) if order_lines.fetch(product_id) <= 0
index_of_line_to_remove = order_lines.index { |line| line.fetch(:product_id) == product_id && line.fetch(:price) == event.data.fetch(:price) }
@order_lines.delete_at(index_of_line_to_remove)
@current_free_product_id = nil if event.data.fetch(:price) == 0
when Pricing::ProductMadeFreeForOrder
@current_free_product_id = product_id
when Pricing::FreeProductRemovedFromOrder
@current_free_product_id = nil
end
end

def total_quantity
order_lines.values.sum
end
end

class FreeProductResolver
MIN_ORDER_LINES_QUANTITY = 4

def initialize(state, pricing_catalog)
@state = state
@pricing_catalog = pricing_catalog
end

def call
cheapest_product if eligible_for_free_product?
end

private

attr_reader :state, :pricing_catalog

def cheapest_product
state.order_lines.keys.sort_by { |product_id| pricing_catalog.price_by_product_id(product_id) }.first
end

def eligible_for_free_product?
state.total_quantity >= MIN_ORDER_LINES_QUANTITY
order_lines.sort_by { |line| line.fetch(:price) }.first.fetch(:product_id) if order_lines.size >= MIN_ORDER_LINES_QUANTITY
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion ecommerce/processes/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def customer_id
end

def given(events, store: event_store, process: nil)
events.each do |ev|
events.flatten.each do |ev|
store.append(ev)
process.call(ev) if process
end
Expand Down
90 changes: 32 additions & 58 deletions ecommerce/processes/test/three_plus_one_free_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@ module Processes
class ThreePlusOneFreeTest < Test
cover "Processes::ThreePlusOneFree*"

def setup
skip "ThreePlusOneFree process block us from removing Pricing::PricingCatalog"
end

def test_one_order_line_is_not_eligible_for_free_product
product_id = SecureRandom.uuid
order_id = SecureRandom.uuid
process = ThreePlusOneFree.new(event_store, command_bus)
given(item_added_event(order_id, product_id, 22)).each do |event|
process.call(event)
end
given(item_added_event(order_id, product_id, 22), process:)
assert_no_command
end

Expand All @@ -23,9 +17,7 @@ def test_four_order_lines_are_eligible_for_free_product
order_id = SecureRandom.uuid
process = ThreePlusOneFree.new(event_store, command_bus)
given([set_price(product_id, 20)])
given(item_added_event(order_id, product_id, 20, times: 4)).each do |event|
process.call(event)
end
given(item_added_event(order_id, product_id, 20, times: 4), process:)
assert_command(Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_id))
end

Expand All @@ -34,12 +26,11 @@ def test_remove_free_product_when_order_lines_qtn_is_less_than_four
order_id = SecureRandom.uuid
process = ThreePlusOneFree.new(event_store, command_bus)
given([set_price(product_id, 20)])
given(item_added_event(order_id, product_id, 20, times:4) +
product_made_for_free(order_id, product_id) +
item_removed_event(order_id, product_id, times: 1) +
free_product_removed(order_id, product_id)).each do |event|
process.call(event)
end
given([item_added_event(order_id, product_id, 20, times: 4),
product_made_for_free(order_id, product_id),
item_removed_event(order_id, product_id, 20, times: 1),
free_product_removed(order_id, product_id)
], process:)

assert_all_commands(Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_id),
Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_id))
Expand All @@ -53,14 +44,12 @@ def test_change_free_product_if_new_order_line_is_the_cheapest
given([set_price(product_id, 20)])
given([set_price(cheapest_product_id, 1)])

given(item_added_event(order_id, product_id, 20, times:4) +
product_made_for_free(order_id, product_id) +
item_added_event(order_id, cheapest_product_id, 1) +
free_product_removed(order_id, product_id) +
product_made_for_free(order_id, cheapest_product_id)
).each do |event|
process.call(event)
end
given([item_added_event(order_id, product_id, 20, times: 4),
product_made_for_free(order_id, product_id),
item_added_event(order_id, cheapest_product_id, 1),
free_product_removed(order_id, product_id),
product_made_for_free(order_id, cheapest_product_id)
], process:)

assert_all_commands(Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_id),
Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_id),
Expand All @@ -75,11 +64,10 @@ def test_do_not_change_free_product_if_new_order_line_is_more_expensive
given([set_price(product_id, 20)])
given([set_price(more_expensive_product_id, 50)])

given(item_added_event(order_id, product_id, 20, times: 4) +
product_made_for_free(order_id, product_id) +
item_added_event(order_id, more_expensive_product_id, 50)).each do |event|
process.call(event)
end
given([item_added_event(order_id, product_id, 20, times: 4),
product_made_for_free(order_id, product_id),
item_added_event(order_id, more_expensive_product_id, 50)
], process:)

assert_all_commands(Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_id))
end
Expand All @@ -92,16 +80,15 @@ def test_change_free_product_if_the_cheapest_order_line_is_removed
given([set_price(product_id, 20)])
given([set_price(cheapest_product_id, 1)])

given(item_added_event(order_id, product_id, 20, times: 4) +
product_made_for_free(order_id, product_id) +
item_added_event(order_id, cheapest_product_id, 1) +
free_product_removed(order_id, product_id) +
product_made_for_free(order_id, cheapest_product_id) +
item_removed_event(order_id, cheapest_product_id) +
free_product_removed(order_id, cheapest_product_id) +
product_made_for_free(order_id, product_id)).each do |event|
process.call(event)
end
given([item_added_event(order_id, product_id, 20, times: 4),
product_made_for_free(order_id, product_id),
item_added_event(order_id, cheapest_product_id, 1),
free_product_removed(order_id, product_id),
product_made_for_free(order_id, cheapest_product_id),
item_removed_event(order_id, cheapest_product_id, 1),
free_product_removed(order_id, cheapest_product_id),
product_made_for_free(order_id, product_id),
], process:)

assert_all_commands(Pricing::MakeProductFreeForOrder.new(order_id: order_id, product_id: product_id),
Pricing::RemoveFreeProductFromOrder.new(order_id: order_id, product_id: product_id),
Expand All @@ -114,50 +101,37 @@ def test_change_free_product_if_the_cheapest_order_line_is_removed
private

def set_price(product_id, amount)
Pricing::PriceSet.new(data: { product_id: product_id, price: amount })
Pricing::PriceSet.new(data: { product_id:, price: amount })
end

def item_added_event(order_id, product_id, price, times: 1)
times.times.collect do
Pricing::PriceItemAdded.new(
data: {
order_id: order_id,
product_id: product_id,
price: price
}
data: { order_id:, product_id:, price: }
)
end
end

def item_removed_event(order_id, product_id, times: 1)
def item_removed_event(order_id, product_id, price, times: 1)
times.times.collect do
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: product_id
}
data: { order_id:, product_id:, price: }
)
end
end

def product_made_for_free(order_id, product_id)
[
Pricing::ProductMadeFreeForOrder.new(
data: {
order_id: order_id,
product_id: product_id
}
data: { order_id:, product_id: }
)
]
end

def free_product_removed(order_id, product_id)
[
Pricing::FreeProductRemovedFromOrder.new(
data: {
order_id: order_id,
product_id: product_id
}
data: { order_id:, product_id: }
)
]
end
Expand Down
2 changes: 1 addition & 1 deletion infra/lib/infra/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Types
OrderNumber =
Types::Strict::String.constrained(format: /\A\d{4}\/\d{2}\/\d+\z/i)
Quantity = Types::Strict::Integer.constrained(gt: 0)
Price = Types::Coercible::Decimal.constrained(gt: 0)
Price = Types::Coercible::Decimal.constrained(gteq: 0)
Value = Types::Coercible::Decimal
PercentageDiscount = Types::Coercible::Decimal.constrained(gt: 0, lteq: 100)
CouponDiscount = Types::Coercible::Decimal.constrained(gt: 0, lteq: 100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ def test_remove_item_when_quantity_gt_1
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: product_id
product_id: product_id,
price: 20,
}
)
)
Expand Down Expand Up @@ -93,7 +94,8 @@ def test_remove_item_when_quantity_eq_1
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: product_id
product_id: product_id,
price: 20,
}
)
)
Expand Down Expand Up @@ -169,7 +171,8 @@ def test_remove_item_when_there_is_another_item
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: another_product_id
product_id: another_product_id,
price: 20,
}
)
)
Expand Down
1 change: 1 addition & 0 deletions rails_application/test/orders/broadcast_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def test_broadcast_remove_item_from_basket
data: {
order_id: order_id,
product_id: product_id,
price: 20,
}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def test_remove_item_when_quantity_gt_1
item_removed_from_basket = Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: product_id
product_id: product_id,
price: 20,
}
)
event_store.publish(item_removed_from_basket)
Expand Down Expand Up @@ -93,7 +94,8 @@ def test_remove_item_when_quantity_eq_1
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: product_id
product_id: product_id,
price: 20,
}
)
)
Expand Down Expand Up @@ -169,7 +171,8 @@ def test_remove_item_when_there_is_another_item
Pricing::PriceItemRemoved.new(
data: {
order_id: order_id,
product_id: another_product_id
product_id: another_product_id,
price: 20,
}
)
)
Expand Down