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: 0 additions & 1 deletion ecommerce/processes/.mutant.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ matcher:
- Processes::SyncShipmentFromPricing*
- Processes::SyncInventoryFromOrdering*
- Processes::NotifyPaymentsAboutOrderValue*
- Processes::ThreePlusOneFree*
- Processes::DetermineVatRatesOnOrderPlaced*
120 changes: 47 additions & 73 deletions ecommerce/processes/lib/processes/three_plus_one_free.rb
Original file line number Diff line number Diff line change
@@ -1,96 +1,70 @@
module Processes
class ThreePlusOneFree
include Infra::Retry

def initialize(event_store, command_bus)
@event_store = event_store
@command_bus = command_bus
@event_store.subscribe(
self,
to: [
Pricing::PriceItemAdded,
Pricing::PriceItemRemoved,
Pricing::ProductMadeFreeForOrder,
Pricing::FreeProductRemovedFromOrder
]
)
end

def call(event)
state = build_state(event)
return if event_only_for_state_building?(event)

make_or_remove_free_product(state)
end
ProcessState = Data.define(:lines, :free_product) do
def initialize(lines: [], free_product: nil)
super(lines: lines.freeze, free_product:)
end

private
MIN_ORDER_LINES_QUANTITY = 4

def build_state(event)
with_retry do
stream_name = "ThreePlusOneFreeProcess$#{event.data.fetch(:order_id)}"
past_events = @event_store.read.stream(stream_name).to_a
last_stored = past_events.size - 1
@event_store.link(event.event_id, stream_name: stream_name, expected_version: last_stored)
ProcessState.new(event.data.fetch(:order_id)).tap do |state|
past_events.each { |ev| state.call(ev) }
state.call(event)
def eligible_free_product
if lines.size >= MIN_ORDER_LINES_QUANTITY
lines.sort_by { _1.fetch(:price) }.first.fetch(:product_id)
end
end
end

def make_or_remove_free_product(state)
free_product_id = state.cheapest_product
include Infra::ProcessManager.with_state(ProcessState)

return if current_free_product_not_changed?(free_product_id, state)
subscribes_to(
Pricing::PriceItemAdded,
Pricing::PriceItemRemoved,
Pricing::ProductMadeFreeForOrder,
Pricing::FreeProductRemovedFromOrder
)

remove_old_free_product(state)
make_new_product_for_free(state, free_product_id)
end
private

def event_only_for_state_building?(event)
event.instance_of?(Pricing::FreeProductRemovedFromOrder) || event.instance_of?(Pricing::ProductMadeFreeForOrder)
def apply(event)
product_id = event.data.fetch(:product_id)
case event
when Pricing::PriceItemAdded
lines = (state.lines + [{ product_id:, price: event.data.fetch(:price) }])
state.with(lines:)
when Pricing::PriceItemRemoved
lines = state.lines.dup
index_of_line_to_remove = lines.index { |line| line.fetch(:product_id) == product_id }
lines.delete_at(index_of_line_to_remove)
state.with(lines:)
when Pricing::ProductMadeFreeForOrder
state.with(free_product: product_id)
when Pricing::FreeProductRemovedFromOrder
state.with(free_product: nil)
end
end

def current_free_product_not_changed?(free_product_id, state)
free_product_id == state.current_free_product_id
def act
case [state.free_product, state.eligible_free_product]
in [the_same_product, ^the_same_product]
in [nil, new_free_product]
make_new_product_for_free(new_free_product)
in [old_free_product, *]
remove_old_free_product(old_free_product)
else
end
end

def remove_old_free_product(state)
@command_bus.call(Pricing::RemoveFreeProductFromOrder.new(order_id: state.order_id, product_id: state.current_free_product_id)) if state.current_free_product_id
def remove_old_free_product(product_id)
command_bus.call(Pricing::RemoveFreeProductFromOrder.new(order_id: id, product_id:))
end

def make_new_product_for_free(state, free_product_id)
@command_bus.call(Pricing::MakeProductFreeForOrder.new(order_id: state.order_id, product_id: free_product_id)) if free_product_id
def make_new_product_for_free(product_id)
command_bus.call(Pricing::MakeProductFreeForOrder.new(order_id: id, product_id:))
end

class ProcessState
attr_reader :order_id, :order_lines, :current_free_product_id

def initialize(order_id)
@order_id = order_id
@order_lines = []
end

def call(event)
product_id = event.data.fetch(:product_id)
case event
when Pricing::PriceItemAdded
order_lines << { product_id: event.data.fetch(:product_id), price: event.data.fetch(:price) }
when Pricing::PriceItemRemoved
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

MIN_ORDER_LINES_QUANTITY = 4
def cheapest_product
order_lines.sort_by { |line| line.fetch(:price) }.first.fetch(:product_id) if order_lines.size >= MIN_ORDER_LINES_QUANTITY
end
def fetch_id(event)
event.data.fetch(:order_id)
end
end
end
9 changes: 7 additions & 2 deletions infra/lib/infra/process_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def initialize(event_store, command_bus)
end

def call(event)
@state = initial_state
@id = fetch_id(event)
build_state(event)
act
Expand Down Expand Up @@ -45,8 +46,12 @@ def subscribes_to(*events)
def self.with_state(state_class)

Module.new do
define_method :state do
@state ||= state_class.new
define_method :initial_state do
state_class.new
end

def state
@state ||= initial_state
end

def self.included(host_class)
Expand Down