Skip to content

Commit

Permalink
Add projection and read models
Browse files Browse the repository at this point in the history
  • Loading branch information
marlena-b committed Jan 6, 2025
1 parent 119b320 commit 5faafcb
Show file tree
Hide file tree
Showing 17 changed files with 306 additions and 168 deletions.
1 change: 1 addition & 0 deletions ecommerce/ordering/lib/ordering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require_relative "ordering/service"
require_relative "ordering/order"
require_relative "ordering/refund"
require_relative "ordering/projections"

module Ordering
class Configuration
Expand Down
11 changes: 11 additions & 0 deletions ecommerce/ordering/lib/ordering/projections.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Ordering
class Projections
def self.product_quantity_available_to_refund(order_id, product_id)
RubyEventStore::Projection
.from_stream("Ordering::Order$#{order_id}")
.init(-> { { available: 0 } })
.when(ItemAddedToBasket, -> (state, event) { state[:available] += 1 if event.data.fetch(:product_id) == product_id })
.when(ItemRemovedFromBasket, -> (state, event) { state[:available] -= 1 if event.data.fetch(:product_id) == product_id })
end
end
end
12 changes: 10 additions & 2 deletions ecommerce/ordering/lib/ordering/refund.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Ordering
class Refund
include AggregateRoot

ExceedsOrderQuantityError = Class.new(StandardError)
ProductNotFoundError = Class.new(StandardError)

def initialize(id)
Expand All @@ -13,7 +14,8 @@ def create_draft(order_id)
apply DraftRefundCreated.new(data: { refund_id: @id, order_id: order_id })
end

def add_item(product_id)
def add_item(product_id, available_quantity_to_refund)
raise ExceedsOrderQuantityError unless enough_items?(available_quantity_to_refund, product_id)
apply ItemAddedToRefund.new(data: { refund_id: @id, order_id: @order_id, product_id: product_id })
end

Expand All @@ -33,6 +35,12 @@ def remove_item(product_id)
on ItemRemovedFromRefund do |event|
@refund_items.decrease_quantity(event.data[:product_id])
end

private

def enough_items?(available_quantity_to_refund, product_id)
@refund_items.quantity(product_id) < available_quantity_to_refund
end
end

class ItemsList
Expand All @@ -48,7 +56,7 @@ def increase_quantity(product_id)

def decrease_quantity(product_id)
refund_items[product_id] -= 1
refund_items.delete(product_id) if refund_items.fetch(product_id).equal?(0)
refund_items.delete(product_id) if quantity(product_id).equal?(0)
end

def quantity(product_id)
Expand Down
15 changes: 14 additions & 1 deletion ecommerce/ordering/lib/ordering/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,26 @@ def call(command)
class OnAddItemToRefund
def initialize(event_store)
@repository = Infra::AggregateRootRepository.new(event_store)
@event_store = event_store
end

def call(command)
@repository.with_aggregate(Refund, command.aggregate_id) do |refund|
refund.add_item(command.product_id)
refund.add_item(
command.product_id,
available_quantity_to_refund(command.order_id, command.product_id)
)
end
end

private

def available_quantity_to_refund(order_id, product_id)
Projections
.product_quantity_available_to_refund(order_id, product_id)
.run(@event_store)
.fetch(:available)
end
end

class OnRemoveItemFromRefund
Expand Down
21 changes: 21 additions & 0 deletions ecommerce/ordering/test/add_item_to_refund_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def test_add_item_to_refund
stream = "Ordering::Refund$#{aggregate_id}"

arrange(
AddItemToBasket.new(order_id: order_id, product_id: product_id),
CreateDraftRefund.new(
refund_id: aggregate_id,
order_id: order_id
Expand All @@ -37,5 +38,25 @@ def test_add_item_to_refund
)
end
end

def test_add_item_raises_exceeds_order_quantity_error
aggregate_id = SecureRandom.uuid
order_id = SecureRandom.uuid
product_id = SecureRandom.uuid

arrange(
AddItemToBasket.new(order_id: order_id, product_id: product_id),
CreateDraftRefund.new(refund_id: aggregate_id, order_id: order_id),
AddItemToRefund.new(
refund_id: aggregate_id,
order_id: order_id,
product_id: product_id
)
)

assert_raises(Ordering::Refund::ExceedsOrderQuantityError) do
act(AddItemToRefund.new(refund_id: aggregate_id, order_id: order_id, product_id: product_id))
end
end
end
end
27 changes: 27 additions & 0 deletions ecommerce/ordering/test/projections_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require_relative "test_helper"

module Ordering
class ProjectionsTest < Test
cover "Ordering::Projections"

def test_product_quantity_available_to_refund
order_id = SecureRandom.uuid
product_id = SecureRandom.uuid
another_product_id = SecureRandom.uuid
stream_name = "Ordering::Order$#{order_id}"
projection = Projections.product_quantity_available_to_refund(order_id, product_id)

event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)

event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: another_product_id }), stream_name: stream_name)
event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: another_product_id }), stream_name: stream_name)

available_quantity_to_refund = projection.run(event_store)

assert_equal({ available: 1 }, available_quantity_to_refund)
end
end
end
48 changes: 48 additions & 0 deletions ecommerce/ordering/test/refund_items_list_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require_relative "test_helper"

module Ordering
class RefundItemsListTest < Test

def test_initialize
list = ItemsList.new

assert_equal 0, list.refund_items.size
end

def test_increase_item_quantity
product_one_id = SecureRandom.uuid
product_two_id = SecureRandom.uuid
list = ItemsList.new

list.increase_quantity(product_one_id)

assert_equal 1, list.refund_items.size
assert_equal 1, list.quantity(product_one_id)

list.increase_quantity(product_two_id)

assert_equal 2, list.refund_items.size
assert_equal 1, list.quantity(product_two_id)
end

def test_decrease_item_quantity
product_id = SecureRandom.uuid
list = ItemsList.new

list.increase_quantity(product_id)
list.increase_quantity(product_id)

assert_equal 1, list.refund_items.size
assert_equal 2, list.quantity(product_id)

list.decrease_quantity(product_id)

assert_equal 1, list.refund_items.size
assert_equal 1, list.quantity(product_id)

list.decrease_quantity(product_id)

assert_equal 0, list.refund_items.size
end
end
end
14 changes: 13 additions & 1 deletion ecommerce/ordering/test/remove_item_from_refund_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def test_removing_items_from_refund
stream = "Ordering::Refund$#{aggregate_id}"

arrange(
AddItemToBasket.new(order_id: order_id, product_id: product_id),
CreateDraftRefund.new(
refund_id: aggregate_id,
order_id: order_id
Expand Down Expand Up @@ -43,15 +44,26 @@ def test_removing_items_from_refund
end
end

def test_can_remove_only_added_items
def test_cant_remove_item_with_0_quantity
order_id = SecureRandom.uuid
aggregate_id = SecureRandom.uuid
product_id = SecureRandom.uuid

arrange(
AddItemToBasket.new(order_id: order_id, product_id: product_id),
CreateDraftRefund.new(
refund_id: aggregate_id,
order_id: order_id
),
AddItemToRefund.new(
refund_id: aggregate_id,
order_id: order_id,
product_id: product_id
),
RemoveItemFromRefund.new(
refund_id: aggregate_id,
order_id: order_id,
product_id: product_id
)
)

Expand Down
27 changes: 25 additions & 2 deletions rails_application/app/controllers/refunds_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class RefundsController < ApplicationController
def edit
@refund = Refunds::Refund.find_by_uid!(params[:id])
@order = Orders::Order.find_by_uid(@refund.order_uid)
@order_lines = @order.order_lines
@order = Orders::Order.find_by_uid!(@refund.order_uid)
@refund_items = build_refund_items_list(@order.order_lines, @refund.refund_items)
end

def create
Expand All @@ -14,10 +14,18 @@ def create

def add_item
add_item_to_refund
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
rescue Ordering::Refund::ExceedsOrderQuantityError
flash[:alert] = "You cannot add more of this product to the refund than is in the original order."
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
end

def remove_item
remove_item_from_refund
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
rescue Ordering::Refund::ProductNotFoundError
flash[:alert] = "This product is not added to the refund."
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
end

private
Expand Down Expand Up @@ -45,4 +53,19 @@ def remove_item_from_refund_cmd
def remove_item_from_refund
command_bus.(remove_item_from_refund_cmd)
end

def build_refund_items_list(order_lines, refund_items)
order_lines.map { |order_line| build_refund_item(order_line, refund_items) }
end

def build_refund_item(order_line, refund_items)
refund_item = refund_items.find { |item| item.product_uid == order_line.product_id } || initialize_refund_item(order_line)

refund_item.order_line = order_line
refund_item
end

def initialize_refund_item(order_line)
Refunds::RefundItem.new(product_uid: order_line.product_id, quantity: 0, price: order_line.price)
end
end
43 changes: 12 additions & 31 deletions rails_application/app/read_models/refunds/add_item_to_refund.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
module Refunds
class AddItemToRefund
def call(event)
refund_id = event.data.fetch(:refund_id)
Refund.find_or_create_by!(uid: refund_id)
product_id = event.data.fetch(:product_id)
item =
find(refund_id, product_id) ||
create(refund_id, product_id)
item.quantity += 1
item.save!
end

private
refund = Refund.find_by!(uid: event.data.fetch(:refund_id))
product = Orders::Product.find_by!(uid: event.data.fetch(:product_id))

def event_store
Rails.configuration.event_store
end
item = refund.refund_items.find_or_create_by(product_uid: product.uid) do |item|
item.price = product.price
item.quantity = 0
end

def find(refund_id, product_id)
Refund
.find_by_uid(refund_id)
.refund_items
.where(product_uid: product_id)
.first
end
refund.total_value += item.price
item.quantity += 1

def create(refund_id, product_id)
product = Orders::Product.find_by_uid(product_id)
Refund
.find_by(uid: refund_id)
.refund_items
.create(
product_uid: product_id,
price: product.price,
quantity: 0
)
ActiveRecord::Base.transaction do
refund.save!
item.save!
end
end
end
end
15 changes: 15 additions & 0 deletions rails_application/app/read_models/refunds/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ class Refund < ApplicationRecord

class RefundItem < ApplicationRecord
self.table_name = "refund_items"

attr_accessor :order_line
delegate :product_name, to: :order_line

def max_quantity?
quantity == order_quantity
end

def order_quantity
order_line.quantity
end

def value
quantity * price
end
end

class Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
module Refunds
class RemoveItemFromRefund
def call(event)
refund_id = event.data.fetch(:refund_id)
product_id = event.data.fetch(:product_id)
item = find(refund_id, product_id)
item.quantity -= 1
item.quantity > 0 ? item.save! : item.destroy!
end
refund = Refund.find_by!(uid: event.data.fetch(:refund_id))
item = refund.refund_items.find_by!(product_uid: event.data.fetch(:product_id))

private
def find(order_uid, product_id)
Refund
.find_by_uid(order_uid)
.refund_items
.where(product_uid: product_id)
.first
end
refund.total_value -= item.price
item.quantity -= 1

def event_store
Rails.configuration.event_store
ActiveRecord::Base.transaction do
refund.save!
item.quantity > 0 ? item.save! : item.destroy!
end
end
end
end
Loading

0 comments on commit 5faafcb

Please sign in to comment.