Skip to content

Commit c708d64

Browse files
Made OrderItemInvoicingProcess more readable
- Used Infra::ProcessManager - Slightly changed the way we call MoneySplitter (it was too generic) - the process is now mutation tested (apart from MoneySplitter) - removed left over DetermineVatRates include (why mutant doesnt catch it?) - reduced defensive checks in MoneySplitter - if we control how we call it and we mutation test it, its safe enough
1 parent 9be321d commit c708d64

File tree

5 files changed

+115
-81
lines changed

5 files changed

+115
-81
lines changed

ecommerce/processes/.mutant.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ matcher:
1212
- Processes::OrderConfirmation#stream_name
1313
- Processes::Test*
1414
- Processes::ReleasePaymentProcess*
15-
- Processes::OrderItemInvoicingProcess*
15+
- Processes::MoneySplitter*
16+
- Processes::OrderItemInvoicingProcess#fetch_id
1617
- Processes::SyncShipmentFromPricing*
1718
- Processes::SyncInventoryFromOrdering*
1819
- Processes::NotifyPaymentsAboutOrderValue*

ecommerce/processes/lib/processes/determine_vat_rates_on_order_placed.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module Processes
22
class DetermineVatRatesOnOrderPlaced
3-
include Infra::Retry
43

54
ProcessState = Data.define(:offer_accepted, :order_placed, :order_id, :order_lines) do
65
def initialize(offer_accepted: false, order_placed: false, order_id: nil, order_lines: [])

ecommerce/processes/lib/processes/order_item_invoicing_process.rb

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
module Processes
22
class OrderItemInvoicingProcess
3-
include Infra::Retry
43

5-
def initialize(event_store, command_bus)
6-
@event_store = event_store
7-
@command_bus = command_bus
4+
ProcessState = Data.define(:order_id, :product_id, :quantity, :vat_rate, :discounted_amount) do
5+
def initialize(order_id: nil, product_id: nil, quantity: nil, vat_rate: nil, discounted_amount: nil)
6+
super(order_id:, product_id:, quantity:, vat_rate:, discounted_amount:)
7+
end
8+
9+
def can_create_invoice_item?
10+
order_id && product_id && quantity && vat_rate && discounted_amount
11+
end
812
end
913

10-
def call(event)
11-
state = build_state(event)
12-
return unless state.create_invoice_item?
14+
include Infra::ProcessManager.with_state(ProcessState)
1315

14-
unit_prices = MoneySplitter.new(state.discounted_amount, Array.new(state.quantity, 1)).call
16+
subscribes_to(
17+
Pricing::PriceItemValueCalculated,
18+
Taxes::VatRateDetermined
19+
)
20+
21+
def apply(event)
22+
case event
23+
when Pricing::PriceItemValueCalculated
24+
state.with(
25+
order_id: event.data.fetch(:order_id),
26+
product_id: event.data.fetch(:product_id),
27+
quantity: event.data.fetch(:quantity),
28+
discounted_amount: event.data.fetch(:discounted_amount)
29+
)
30+
when Taxes::VatRateDetermined
31+
state.with(
32+
vat_rate: event.data.fetch(:vat_rate)
33+
)
34+
end
35+
end
36+
37+
def act
38+
return unless state.can_create_invoice_item?
39+
40+
unit_prices = MoneySplitter.new(state.discounted_amount, state.quantity).call
1541
unit_prices.tally.each do |unit_price, quantity|
1642
command_bus.call(Invoicing::AddInvoiceItem.new(
1743
invoice_id: state.order_id,
@@ -25,48 +51,15 @@ def call(event)
2551

2652
private
2753

28-
attr_reader :event_store, :command_bus
29-
30-
def build_state(event)
31-
with_retry do
32-
stream_name = "OrderInvoicingProcess$#{event.data.fetch(:order_id)}$#{event.data.fetch(:product_id)}"
33-
past = event_store.read.stream(stream_name).to_a
34-
last_stored = past.size - 1
35-
event_store.link(event.event_id, stream_name: stream_name, expected_version: last_stored)
36-
ProcessState.new.tap do |state|
37-
past.each { |ev| state.call(ev) }
38-
state.call(event)
39-
end
40-
end
41-
end
42-
43-
class ProcessState
44-
attr_reader :order_id, :product_id, :quantity, :vat_rate, :discounted_amount
45-
46-
def call(event)
47-
@order_id ||= event.data.fetch(:order_id)
48-
@product_id ||= event.data.fetch(:product_id)
49-
case event
50-
when Pricing::PriceItemValueCalculated
51-
@quantity = event.data.fetch(:quantity)
52-
@discounted_amount = event.data.fetch(:discounted_amount)
53-
when Taxes::VatRateDetermined
54-
@vat_rate = event.data.fetch(:vat_rate).symbolize_keys
55-
end
56-
end
57-
58-
def create_invoice_item?
59-
[order_id, product_id, quantity, vat_rate, discounted_amount].all?
60-
end
54+
def fetch_id(event)
55+
"#{event.data.fetch(:order_id)}$#{event.data.fetch(:product_id)}"
6156
end
6257
end
6358

6459
class MoneySplitter
65-
def initialize(amount, weights)
66-
raise ArgumentError unless weights.instance_of? Array
67-
raise ArgumentError if weights.empty?
60+
def initialize(amount, quantity)
6861
@amount = amount
69-
@weights = weights
62+
@weights = Array.new(quantity, 1)
7063
end
7164

7265
def call

ecommerce/processes/test/money_splitter_test.rb

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,9 @@ class MoneySplitterTest < Minitest::Test
55
cover "Processes::MoneySplitter"
66

77
def test_splitting_money_without_losing_cents
8-
assert_equal([0.01, 0.01, 0.01], MoneySplitter.new(0.03, [1, 1, 1]).call)
9-
assert_equal([0.01, 0.02], MoneySplitter.new(0.03, [1, 1]).call.sort)
10-
assert_equal([0, 0, 0.01, 0.01, 0.01], MoneySplitter.new(0.03, [1, 1, 1, 1, 1]).call.sort)
11-
assert_equal([0, 0, 0.03], MoneySplitter.new(0.03, [1, 0, 0]).call.sort)
12-
13-
assert_raises(ArgumentError) { MoneySplitter.new(0.03, nil).call }
14-
assert_raises(ArgumentError) { MoneySplitter.new(0.03, 'not nil nor array').call }
15-
assert_raises(ArgumentError) { MoneySplitter.new(0.03, []).call }
8+
assert_equal([0.01, 0.01, 0.01], MoneySplitter.new(0.03, 3).call)
9+
assert_equal([0.01, 0.02], MoneySplitter.new(0.03, 2).call.sort)
10+
assert_equal([0, 0, 0.01, 0.01, 0.01], MoneySplitter.new(0.03, 5).call.sort)
1611
end
1712
end
1813
end

ecommerce/processes/test/order_item_invoicing_process_test.rb

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,86 @@ module Processes
44
class OrderItemInvoicingProcessTest < Test
55
cover "Processes::OrderItemInvoicingProcess*"
66

7-
def test_invoice_item_being_created
8-
product_id = SecureRandom.uuid
9-
amount = 100.to_d
10-
discounted_amount = 90.to_d
11-
quantity = 5
12-
vat_rate = Infra::Types::VatRate.new(rate: 20, code: "20")
7+
def setup
8+
super
9+
@product_id = SecureRandom.uuid
10+
@amount = 100.to_d
11+
@discounted_amount = 90.to_d
12+
@quantity = 5
13+
@vat_rate = Infra::Types::VatRate.new(rate: 20, code: "20")
14+
end
1315

14-
item_value_calculated = Pricing::PriceItemValueCalculated.new(
15-
data: {
16-
order_id: order_id,
17-
product_id: product_id,
18-
quantity: quantity,
19-
amount: amount,
20-
discounted_amount: discounted_amount
21-
}
22-
)
23-
vat_rate_determined = Taxes::VatRateDetermined.new(
24-
data: {
25-
order_id: order_id,
26-
product_id: product_id,
27-
vat_rate: vat_rate
28-
}
29-
)
16+
def test_invoice_item_being_created
3017
process = OrderItemInvoicingProcess.new(event_store, command_bus)
31-
given([item_value_calculated, vat_rate_determined]).each do |event|
18+
given([
19+
Pricing::PriceItemValueCalculated.new(
20+
data: {
21+
order_id: order_id,
22+
product_id: @product_id,
23+
quantity: @quantity,
24+
amount: @amount,
25+
discounted_amount: @discounted_amount
26+
}
27+
),
28+
Taxes::VatRateDetermined.new(
29+
data: {
30+
order_id: order_id,
31+
product_id: @product_id,
32+
vat_rate: @vat_rate
33+
}
34+
)]).each do |event|
3235
process.call(event)
3336
end
3437
assert_command(Invoicing::AddInvoiceItem.new(
3538
invoice_id: order_id,
36-
product_id: product_id,
37-
quantity: quantity,
38-
vat_rate: vat_rate,
39+
product_id: @product_id,
40+
quantity: @quantity,
41+
vat_rate: @vat_rate,
3942
unit_price: 18.to_d
4043
))
4144
end
4245
end
46+
47+
def test_vat_rate_comes_first
48+
process = OrderItemInvoicingProcess.new(event_store, command_bus)
49+
given([
50+
Taxes::VatRateDetermined.new(
51+
data: {
52+
order_id: order_id,
53+
product_id: @product_id,
54+
vat_rate: @vat_rate
55+
}
56+
),
57+
Pricing::PriceItemValueCalculated.new(
58+
data: {
59+
order_id: order_id,
60+
product_id: @product_id,
61+
quantity: @quantity,
62+
amount: @amount,
63+
discounted_amount: @discounted_amount
64+
}
65+
)
66+
]).each do |event|
67+
process.call(event)
68+
end
69+
assert_command(Invoicing::AddInvoiceItem.new(
70+
invoice_id: order_id,
71+
product_id: @product_id,
72+
quantity: @quantity,
73+
vat_rate: @vat_rate,
74+
unit_price: 18.to_d
75+
))
76+
end
77+
78+
def test_stream_name
79+
process = OrderItemInvoicingProcess.new(event_store, command_bus)
80+
given([Pricing::PriceItemValueCalculated.new(data: { order_id: order_id,
81+
product_id: product_id,
82+
quantity: quantity,
83+
amount: amount,
84+
discounted_amount: discounted_amount })]).each do |event|
85+
process.call(event)
86+
end
87+
assert_equal "Processes::OrderItemInvoicingProcess$#{order_id}", process.send(:stream_name)
88+
end
4389
end

0 commit comments

Comments
 (0)