diff --git a/ecommerce/ordering/lib/ordering.rb b/ecommerce/ordering/lib/ordering.rb index d89c2140..2f0be385 100644 --- a/ecommerce/ordering/lib/ordering.rb +++ b/ecommerce/ordering/lib/ordering.rb @@ -22,7 +22,7 @@ require_relative "ordering/service" require_relative "ordering/order" require_relative "ordering/refund" -require_relative "ordering/product_quantity_available_to_refund" +require_relative "ordering/refundable_products" module Ordering class Configuration diff --git a/ecommerce/ordering/lib/ordering/events/draft_refund_created.rb b/ecommerce/ordering/lib/ordering/events/draft_refund_created.rb index 147700f7..f10f3136 100644 --- a/ecommerce/ordering/lib/ordering/events/draft_refund_created.rb +++ b/ecommerce/ordering/lib/ordering/events/draft_refund_created.rb @@ -2,5 +2,11 @@ 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 diff --git a/ecommerce/ordering/lib/ordering/product_quantity_available_to_refund.rb b/ecommerce/ordering/lib/ordering/product_quantity_available_to_refund.rb deleted file mode 100644 index 990b8156..00000000 --- a/ecommerce/ordering/lib/ordering/product_quantity_available_to_refund.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Ordering - class ProductQuantityAvailableToRefund - def self.call(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 diff --git a/ecommerce/ordering/lib/ordering/refund.rb b/ecommerce/ordering/lib/ordering/refund.rb index 858ca243..64a2f503 100644 --- a/ecommerce/ordering/lib/ordering/refund.rb +++ b/ecommerce/ordering/lib/ordering/refund.rb @@ -10,12 +10,12 @@ def initialize(id) @refund_items = ItemsList.new end - def create_draft(order_id) - apply DraftRefundCreated.new(data: { refund_id: @id, order_id: order_id }) + 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, available_quantity_to_refund) - raise ExceedsOrderQuantityError unless enough_items?(available_quantity_to_refund, product_id) + 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 @@ -26,6 +26,7 @@ def remove_item(product_id) on DraftRefundCreated do |event| @order_id = event.data[:order_id] + @refundable_products = event.data[:refundable_products] end on ItemAddedToRefund do |event| @@ -38,8 +39,13 @@ def remove_item(product_id) private - def enough_items?(available_quantity_to_refund, product_id) - @refund_items.quantity(product_id) < available_quantity_to_refund + 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 diff --git a/ecommerce/ordering/lib/ordering/refundable_products.rb b/ecommerce/ordering/lib/ordering/refundable_products.rb new file mode 100644 index 00000000..d436bc7e --- /dev/null +++ b/ecommerce/ordering/lib/ordering/refundable_products.rb @@ -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 diff --git a/ecommerce/ordering/lib/ordering/service.rb b/ecommerce/ordering/lib/ordering/service.rb index 014528a0..02f2373b 100644 --- a/ecommerce/ordering/lib/ordering/service.rb +++ b/ecommerce/ordering/lib/ordering/service.rb @@ -76,38 +76,37 @@ def call(command) 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) + 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) - @event_store = event_store end def call(command) @repository.with_aggregate(Refund, command.aggregate_id) do |refund| - refund.add_item( - command.product_id, - available_quantity_to_refund(command.order_id, command.product_id) - ) + refund.add_item(command.product_id) end end - - private - - def available_quantity_to_refund(order_id, product_id) - ProductQuantityAvailableToRefund - .call(order_id, product_id) - .run(@event_store) - .fetch(:available) - end end class OnRemoveItemFromRefund diff --git a/ecommerce/ordering/test/add_item_to_refund_test.rb b/ecommerce/ordering/test/add_item_to_refund_test.rb index 51dd8b55..7ce8f9b9 100644 --- a/ecommerce/ordering/test/add_item_to_refund_test.rb +++ b/ecommerce/ordering/test/add_item_to_refund_test.rb @@ -7,23 +7,30 @@ class AddItemToRefundTest < Test def test_add_item_to_refund order_id = SecureRandom.uuid aggregate_id = SecureRandom.uuid - product_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_id), - CreateDraftRefund.new( - refund_id: aggregate_id, - order_id: order_id + 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_id + product_id: product_2_id } ) ] @@ -33,7 +40,7 @@ def test_add_item_to_refund AddItemToRefund.new( refund_id: aggregate_id, order_id: order_id, - product_id: product_id + product_id: product_2_id ) ) end diff --git a/ecommerce/ordering/test/create_draft_refund_test.rb b/ecommerce/ordering/test/create_draft_refund_test.rb index 77c0856a..3ba34416 100644 --- a/ecommerce/ordering/test/create_draft_refund_test.rb +++ b/ecommerce/ordering/test/create_draft_refund_test.rb @@ -13,7 +13,8 @@ def test_draft_refund_created DraftRefundCreated.new( data: { refund_id: aggregate_id, - order_id: order_id + order_id: order_id, + refundable_products: [] } ) ] diff --git a/ecommerce/ordering/test/product_quantity_available_to_refund_test.rb b/ecommerce/ordering/test/product_quantity_available_to_refund_test.rb deleted file mode 100644 index bbfc5d1d..00000000 --- a/ecommerce/ordering/test/product_quantity_available_to_refund_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "test_helper" - -module Ordering - class ProductQuantityAvailableToRefundTest < Test - cover "Ordering::ProductQuantityAvailableToRefund" - - 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 = ProductQuantityAvailableToRefund.call(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 diff --git a/ecommerce/ordering/test/refundable_products_test.rb b/ecommerce/ordering/test/refundable_products_test.rb new file mode 100644 index 00000000..b9c81091 --- /dev/null +++ b/ecommerce/ordering/test/refundable_products_test.rb @@ -0,0 +1,29 @@ +require_relative "test_helper" + +module Ordering + class RefundableProductsTest < Test + cover "Ordering::RefundableProducts" + + def test_product_quantity_available_to_refund + order_id = SecureRandom.uuid + product_1_id = SecureRandom.uuid + product_2_id = SecureRandom.uuid + product_3_id = SecureRandom.uuid + stream_name = "Ordering::Order$#{order_id}" + projection = RefundableProducts.call(order_id) + + event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new) + + event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_3_id }), stream_name: stream_name) + event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_1_id }), stream_name: stream_name) + event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_2_id }), stream_name: stream_name) + event_store.publish(ItemAddedToBasket.new(data: { order_id: order_id, product_id: product_2_id }), stream_name: stream_name) + event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: product_2_id }), stream_name: stream_name) + event_store.publish(ItemRemovedFromBasket.new(data: { order_id: order_id, product_id: product_3_id }), stream_name: stream_name) + + refundable_products = projection.run(event_store) + + assert_equal([{ product_id: product_1_id, quantity: 1}, {product_id: product_2_id, quantity: 1 }], refundable_products) + end + end +end diff --git a/ecommerce/ordering/test/remove_item_from_refund_test.rb b/ecommerce/ordering/test/remove_item_from_refund_test.rb index fcba5866..ba582e1d 100644 --- a/ecommerce/ordering/test/remove_item_from_refund_test.rb +++ b/ecommerce/ordering/test/remove_item_from_refund_test.rb @@ -7,11 +7,16 @@ class RemoveItemFromRefundTest < Test def test_removing_items_from_refund order_id = SecureRandom.uuid aggregate_id = SecureRandom.uuid - product_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_id), + 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 @@ -19,7 +24,7 @@ def test_removing_items_from_refund AddItemToRefund.new( refund_id: aggregate_id, order_id: order_id, - product_id: product_id + product_id: product_1_id ) ) @@ -28,7 +33,7 @@ def test_removing_items_from_refund data: { refund_id: aggregate_id, order_id: order_id, - product_id: product_id + product_id: product_1_id } ) ] @@ -38,7 +43,7 @@ def test_removing_items_from_refund RemoveItemFromRefund.new( refund_id: aggregate_id, order_id: order_id, - product_id: product_id + product_id: product_1_id ) ) end diff --git a/rails_application/test/orders/item_removed_from_refund_test.rb b/rails_application/test/orders/item_removed_from_refund_test.rb index c64efebc..b5b2ce31 100644 --- a/rails_application/test/orders/item_removed_from_refund_test.rb +++ b/rails_application/test/orders/item_removed_from_refund_test.rb @@ -9,7 +9,8 @@ def test_remove_item_from_refund product_id = SecureRandom.uuid another_product_id = SecureRandom.uuid order_id = SecureRandom.uuid - create_draft_refund(refund_id, order_id) + refundable_products = [{product_id: product_id, quantity: 1}, {product_id: another_product_id, quantity: 1}] + create_draft_refund(refund_id, order_id, refundable_products) prepare_product(product_id, 50) prepare_product(another_product_id, 30) AddItemToRefund.new.call(item_added_to_refund(refund_id, order_id, product_id)) @@ -30,8 +31,8 @@ def test_remove_item_from_refund private - def create_draft_refund(refund_id, order_id) - draft_refund_created = Ordering::DraftRefundCreated.new(data: { refund_id: refund_id, order_id: order_id }) + def create_draft_refund(refund_id, order_id, refundable_products) + draft_refund_created = Ordering::DraftRefundCreated.new(data: { refund_id: refund_id, order_id: order_id, refundable_products: refundable_products }) CreateDraftRefund.new.call(draft_refund_created) end