Skip to content

Commit 5faafcb

Browse files
committed
Add projection and read models
1 parent 119b320 commit 5faafcb

File tree

17 files changed

+306
-168
lines changed

17 files changed

+306
-168
lines changed

ecommerce/ordering/lib/ordering.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
require_relative "ordering/service"
2323
require_relative "ordering/order"
2424
require_relative "ordering/refund"
25+
require_relative "ordering/projections"
2526

2627
module Ordering
2728
class Configuration
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Ordering
2+
class Projections
3+
def self.product_quantity_available_to_refund(order_id, product_id)
4+
RubyEventStore::Projection
5+
.from_stream("Ordering::Order$#{order_id}")
6+
.init(-> { { available: 0 } })
7+
.when(ItemAddedToBasket, -> (state, event) { state[:available] += 1 if event.data.fetch(:product_id) == product_id })
8+
.when(ItemRemovedFromBasket, -> (state, event) { state[:available] -= 1 if event.data.fetch(:product_id) == product_id })
9+
end
10+
end
11+
end

ecommerce/ordering/lib/ordering/refund.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Ordering
22
class Refund
33
include AggregateRoot
44

5+
ExceedsOrderQuantityError = Class.new(StandardError)
56
ProductNotFoundError = Class.new(StandardError)
67

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

16-
def add_item(product_id)
17+
def add_item(product_id, available_quantity_to_refund)
18+
raise ExceedsOrderQuantityError unless enough_items?(available_quantity_to_refund, product_id)
1719
apply ItemAddedToRefund.new(data: { refund_id: @id, order_id: @order_id, product_id: product_id })
1820
end
1921

@@ -33,6 +35,12 @@ def remove_item(product_id)
3335
on ItemRemovedFromRefund do |event|
3436
@refund_items.decrease_quantity(event.data[:product_id])
3537
end
38+
39+
private
40+
41+
def enough_items?(available_quantity_to_refund, product_id)
42+
@refund_items.quantity(product_id) < available_quantity_to_refund
43+
end
3644
end
3745

3846
class ItemsList
@@ -48,7 +56,7 @@ def increase_quantity(product_id)
4856

4957
def decrease_quantity(product_id)
5058
refund_items[product_id] -= 1
51-
refund_items.delete(product_id) if refund_items.fetch(product_id).equal?(0)
59+
refund_items.delete(product_id) if quantity(product_id).equal?(0)
5260
end
5361

5462
def quantity(product_id)

ecommerce/ordering/lib/ordering/service.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,26 @@ def call(command)
8888
class OnAddItemToRefund
8989
def initialize(event_store)
9090
@repository = Infra::AggregateRootRepository.new(event_store)
91+
@event_store = event_store
9192
end
9293

9394
def call(command)
9495
@repository.with_aggregate(Refund, command.aggregate_id) do |refund|
95-
refund.add_item(command.product_id)
96+
refund.add_item(
97+
command.product_id,
98+
available_quantity_to_refund(command.order_id, command.product_id)
99+
)
96100
end
97101
end
102+
103+
private
104+
105+
def available_quantity_to_refund(order_id, product_id)
106+
Projections
107+
.product_quantity_available_to_refund(order_id, product_id)
108+
.run(@event_store)
109+
.fetch(:available)
110+
end
98111
end
99112

100113
class OnRemoveItemFromRefund

ecommerce/ordering/test/add_item_to_refund_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def test_add_item_to_refund
1111
stream = "Ordering::Refund$#{aggregate_id}"
1212

1313
arrange(
14+
AddItemToBasket.new(order_id: order_id, product_id: product_id),
1415
CreateDraftRefund.new(
1516
refund_id: aggregate_id,
1617
order_id: order_id
@@ -37,5 +38,25 @@ def test_add_item_to_refund
3738
)
3839
end
3940
end
41+
42+
def test_add_item_raises_exceeds_order_quantity_error
43+
aggregate_id = SecureRandom.uuid
44+
order_id = SecureRandom.uuid
45+
product_id = SecureRandom.uuid
46+
47+
arrange(
48+
AddItemToBasket.new(order_id: order_id, product_id: product_id),
49+
CreateDraftRefund.new(refund_id: aggregate_id, order_id: order_id),
50+
AddItemToRefund.new(
51+
refund_id: aggregate_id,
52+
order_id: order_id,
53+
product_id: product_id
54+
)
55+
)
56+
57+
assert_raises(Ordering::Refund::ExceedsOrderQuantityError) do
58+
act(AddItemToRefund.new(refund_id: aggregate_id, order_id: order_id, product_id: product_id))
59+
end
60+
end
4061
end
4162
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require_relative "test_helper"
2+
3+
module Ordering
4+
class ProjectionsTest < Test
5+
cover "Ordering::Projections"
6+
7+
def test_product_quantity_available_to_refund
8+
order_id = SecureRandom.uuid
9+
product_id = SecureRandom.uuid
10+
another_product_id = SecureRandom.uuid
11+
stream_name = "Ordering::Order$#{order_id}"
12+
projection = Projections.product_quantity_available_to_refund(order_id, product_id)
13+
14+
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
15+
16+
event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
17+
event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
18+
event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: product_id }), stream_name: stream_name)
19+
event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: another_product_id }), stream_name: stream_name)
20+
event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: another_product_id }), stream_name: stream_name)
21+
22+
available_quantity_to_refund = projection.run(event_store)
23+
24+
assert_equal({ available: 1 }, available_quantity_to_refund)
25+
end
26+
end
27+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require_relative "test_helper"
2+
3+
module Ordering
4+
class RefundItemsListTest < Test
5+
6+
def test_initialize
7+
list = ItemsList.new
8+
9+
assert_equal 0, list.refund_items.size
10+
end
11+
12+
def test_increase_item_quantity
13+
product_one_id = SecureRandom.uuid
14+
product_two_id = SecureRandom.uuid
15+
list = ItemsList.new
16+
17+
list.increase_quantity(product_one_id)
18+
19+
assert_equal 1, list.refund_items.size
20+
assert_equal 1, list.quantity(product_one_id)
21+
22+
list.increase_quantity(product_two_id)
23+
24+
assert_equal 2, list.refund_items.size
25+
assert_equal 1, list.quantity(product_two_id)
26+
end
27+
28+
def test_decrease_item_quantity
29+
product_id = SecureRandom.uuid
30+
list = ItemsList.new
31+
32+
list.increase_quantity(product_id)
33+
list.increase_quantity(product_id)
34+
35+
assert_equal 1, list.refund_items.size
36+
assert_equal 2, list.quantity(product_id)
37+
38+
list.decrease_quantity(product_id)
39+
40+
assert_equal 1, list.refund_items.size
41+
assert_equal 1, list.quantity(product_id)
42+
43+
list.decrease_quantity(product_id)
44+
45+
assert_equal 0, list.refund_items.size
46+
end
47+
end
48+
end

ecommerce/ordering/test/remove_item_from_refund_test.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def test_removing_items_from_refund
1111
stream = "Ordering::Refund$#{aggregate_id}"
1212

1313
arrange(
14+
AddItemToBasket.new(order_id: order_id, product_id: product_id),
1415
CreateDraftRefund.new(
1516
refund_id: aggregate_id,
1617
order_id: order_id
@@ -43,15 +44,26 @@ def test_removing_items_from_refund
4344
end
4445
end
4546

46-
def test_can_remove_only_added_items
47+
def test_cant_remove_item_with_0_quantity
4748
order_id = SecureRandom.uuid
4849
aggregate_id = SecureRandom.uuid
4950
product_id = SecureRandom.uuid
5051

5152
arrange(
53+
AddItemToBasket.new(order_id: order_id, product_id: product_id),
5254
CreateDraftRefund.new(
5355
refund_id: aggregate_id,
5456
order_id: order_id
57+
),
58+
AddItemToRefund.new(
59+
refund_id: aggregate_id,
60+
order_id: order_id,
61+
product_id: product_id
62+
),
63+
RemoveItemFromRefund.new(
64+
refund_id: aggregate_id,
65+
order_id: order_id,
66+
product_id: product_id
5567
)
5668
)
5769

rails_application/app/controllers/refunds_controller.rb

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
class RefundsController < ApplicationController
22
def edit
33
@refund = Refunds::Refund.find_by_uid!(params[:id])
4-
@order = Orders::Order.find_by_uid(@refund.order_uid)
5-
@order_lines = @order.order_lines
4+
@order = Orders::Order.find_by_uid!(@refund.order_uid)
5+
@refund_items = build_refund_items_list(@order.order_lines, @refund.refund_items)
66
end
77

88
def create
@@ -14,10 +14,18 @@ def create
1414

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

1923
def remove_item
2024
remove_item_from_refund
25+
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
26+
rescue Ordering::Refund::ProductNotFoundError
27+
flash[:alert] = "This product is not added to the refund."
28+
redirect_to edit_order_refund_path(params[:id], order_id: params[:order_id])
2129
end
2230

2331
private
@@ -45,4 +53,19 @@ def remove_item_from_refund_cmd
4553
def remove_item_from_refund
4654
command_bus.(remove_item_from_refund_cmd)
4755
end
56+
57+
def build_refund_items_list(order_lines, refund_items)
58+
order_lines.map { |order_line| build_refund_item(order_line, refund_items) }
59+
end
60+
61+
def build_refund_item(order_line, refund_items)
62+
refund_item = refund_items.find { |item| item.product_uid == order_line.product_id } || initialize_refund_item(order_line)
63+
64+
refund_item.order_line = order_line
65+
refund_item
66+
end
67+
68+
def initialize_refund_item(order_line)
69+
Refunds::RefundItem.new(product_uid: order_line.product_id, quantity: 0, price: order_line.price)
70+
end
4871
end
Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
11
module Refunds
22
class AddItemToRefund
33
def call(event)
4-
refund_id = event.data.fetch(:refund_id)
5-
Refund.find_or_create_by!(uid: refund_id)
6-
product_id = event.data.fetch(:product_id)
7-
item =
8-
find(refund_id, product_id) ||
9-
create(refund_id, product_id)
10-
item.quantity += 1
11-
item.save!
12-
end
13-
14-
private
4+
refund = Refund.find_by!(uid: event.data.fetch(:refund_id))
5+
product = Orders::Product.find_by!(uid: event.data.fetch(:product_id))
156

16-
def event_store
17-
Rails.configuration.event_store
18-
end
7+
item = refund.refund_items.find_or_create_by(product_uid: product.uid) do |item|
8+
item.price = product.price
9+
item.quantity = 0
10+
end
1911

20-
def find(refund_id, product_id)
21-
Refund
22-
.find_by_uid(refund_id)
23-
.refund_items
24-
.where(product_uid: product_id)
25-
.first
26-
end
12+
refund.total_value += item.price
13+
item.quantity += 1
2714

28-
def create(refund_id, product_id)
29-
product = Orders::Product.find_by_uid(product_id)
30-
Refund
31-
.find_by(uid: refund_id)
32-
.refund_items
33-
.create(
34-
product_uid: product_id,
35-
price: product.price,
36-
quantity: 0
37-
)
15+
ActiveRecord::Base.transaction do
16+
refund.save!
17+
item.save!
18+
end
3819
end
3920
end
4021
end

0 commit comments

Comments
 (0)