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
11 changes: 11 additions & 0 deletions ecommerce/ordering/lib/ordering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@
require_relative "ordering/events/order_expired"
require_relative "ordering/events/order_submitted"
require_relative "ordering/events/order_rejected"
require_relative "ordering/events/draft_refund_created"
require_relative "ordering/events/item_added_to_refund"
require_relative "ordering/events/item_removed_from_refund"
require_relative "ordering/commands/add_item_to_basket"
require_relative "ordering/commands/remove_item_from_basket"
require_relative "ordering/commands/submit_order"
require_relative "ordering/commands/set_order_as_expired"
require_relative "ordering/commands/accept_order"
require_relative "ordering/commands/reject_order"
require_relative "ordering/commands/create_draft_refund"
require_relative "ordering/commands/add_item_to_refund"
require_relative "ordering/commands/remove_item_from_refund"
require_relative "ordering/fake_number_generator"
require_relative "ordering/number_generator"
require_relative "ordering/service"
require_relative "ordering/order"
require_relative "ordering/refund"
require_relative "ordering/refundable_products"

module Ordering
class Configuration
Expand All @@ -32,6 +40,9 @@ def call(event_store, command_bus)
command_bus.register(SetOrderAsExpired, OnSetOrderAsExpired.new(event_store))
command_bus.register(AcceptOrder, OnAcceptOrder.new(event_store))
command_bus.register(RejectOrder, OnRejectOrder.new(event_store))
command_bus.register(CreateDraftRefund, OnCreateDraftRefund.new(event_store))
command_bus.register(AddItemToRefund, OnAddItemToRefund.new(event_store))
command_bus.register(RemoveItemFromRefund, OnRemoveItemFromRefund.new(event_store))
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Ordering
class AddItemToRefund < Infra::Command
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID

alias aggregate_id refund_id
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Ordering
class CreateDraftRefund < Infra::Command
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID

alias aggregate_id refund_id
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Ordering
class RemoveItemFromRefund < Infra::Command
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID

alias aggregate_id refund_id
end
end
12 changes: 12 additions & 0 deletions ecommerce/ordering/lib/ordering/events/draft_refund_created.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Ordering
class DraftRefundCreated < Infra::Event
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :refundable_products, Infra::Types::Array.of(
Infra::Types::Hash.schema(
product_id: Infra::Types::UUID,
quantity: Infra::Types::Integer
)
)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Ordering
class ItemAddedToRefund < Infra::Event
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Ordering
class ItemRemovedFromRefund < Infra::Event
attribute :refund_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
end
end
72 changes: 72 additions & 0 deletions ecommerce/ordering/lib/ordering/refund.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module Ordering
class Refund
include AggregateRoot

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

def initialize(id)
@id = id
@refund_items = ItemsList.new
end

def create_draft(order_id, refundable_products)
apply DraftRefundCreated.new(data: { refund_id: @id, order_id: order_id, refundable_products: refundable_products })
end

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

def remove_item(product_id)
raise RefundHaveNotBeenRequestedForThisProductError unless @refund_items.quantity(product_id).positive?
apply ItemRemovedFromRefund.new(data: { refund_id: @id, order_id: @order_id, product_id: product_id })
end

on DraftRefundCreated do |event|
@order_id = event.data[:order_id]
@refundable_products = event.data[:refundable_products]
end

on ItemAddedToRefund do |event|
@refund_items.increase_quantity(event.data[:product_id])
end

on ItemRemovedFromRefund do |event|
@refund_items.decrease_quantity(event.data[:product_id])
end

private

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

def refundable_quantity(product_id)
product = @refundable_products.find { |product| product.fetch(:product_id) == product_id }
product.fetch(:quantity)
end
end

class ItemsList
attr_reader :refund_items

def initialize
@refund_items = Hash.new(0)
end

def increase_quantity(product_id)
refund_items[product_id] = quantity(product_id) + 1
end

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

def quantity(product_id)
refund_items[product_id]
end
end
end
32 changes: 32 additions & 0 deletions ecommerce/ordering/lib/ordering/refundable_products.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Ordering
class RefundableProducts
class << self
def call(order_id)
RubyEventStore::Projection
.from_stream("Ordering::Order$#{order_id}")
.init(-> { [] })
.when(ItemAddedToBasket, -> (state, event) { increase_quantity(state, event.data.fetch(:product_id)) })
.when(ItemRemovedFromBasket, -> (state, event) { decrease_quantity(state, event.data.fetch(:product_id)) })
end

private

def increase_quantity(state, product_id)
prod_quantity = state.find { |prod_quantity| prod_quantity.fetch(:product_id) == product_id }

if prod_quantity
prod_quantity[:quantity] += 1
else
state << { product_id: product_id, quantity: 1 }
end
end

def decrease_quantity(state, product_id)
prod_quantity = state.find { |prod_quantity| prod_quantity.fetch(:product_id) == product_id }

prod_quantity[:quantity] -= 1
state.delete(prod_quantity) if prod_quantity.fetch(:quantity).zero?
end
end
end
end
48 changes: 48 additions & 0 deletions ecommerce/ordering/lib/ordering/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,52 @@ def call(command)
end
end
end

class OnCreateDraftRefund
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.create_draft(
command.order_id,
refundable_products(command.order_id)
)
end
end

private

def refundable_products(order_id)
RefundableProducts
.call(order_id)
.run(@event_store)
end
end

class OnAddItemToRefund
def initialize(event_store)
@repository = Infra::AggregateRootRepository.new(event_store)
end

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

class OnRemoveItemFromRefund
def initialize(event_store)
@repository = Infra::AggregateRootRepository.new(event_store)
end

def call(command)
@repository.with_aggregate(Refund, command.aggregate_id) do |refund|
refund.remove_item(command.product_id)
end
end
end
end
69 changes: 69 additions & 0 deletions ecommerce/ordering/test/add_item_to_refund_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require_relative "test_helper"

module Ordering
class AddItemToRefundTest < Test
cover "Ordering::OnAddItemToRefund*"

def test_add_item_to_refund
order_id = SecureRandom.uuid
aggregate_id = SecureRandom.uuid
product_1_id = SecureRandom.uuid
product_2_id = SecureRandom.uuid
product_3_id = SecureRandom.uuid
stream = "Ordering::Refund$#{aggregate_id}"

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

expected_events = [
ItemAddedToRefund.new(
data: {
refund_id: aggregate_id,
order_id: order_id,
product_id: product_2_id
}
)
]

assert_events(stream, *expected_events) do
act(
AddItemToRefund.new(
refund_id: aggregate_id,
order_id: order_id,
product_id: product_2_id
)
)
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
32 changes: 32 additions & 0 deletions ecommerce/ordering/test/create_draft_refund_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require_relative "test_helper"

module Ordering
class CreateDraftRefundTest < Test
cover "Ordering::OnCreateDraftRefund*"

def test_draft_refund_created
order_id = SecureRandom.uuid
aggregate_id = SecureRandom.uuid
stream = "Ordering::Refund$#{aggregate_id}"

expected_events = [
DraftRefundCreated.new(
data: {
refund_id: aggregate_id,
order_id: order_id,
refundable_products: []
}
)
]

assert_events(stream, *expected_events) do
act(
CreateDraftRefund.new(
refund_id: aggregate_id,
order_id: order_id
)
)
end
end
end
end
Loading
Loading