Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
151f6e3
Rename Refund to Return
dysk Aug 27, 2025
ae57339
Update module loading.
dysk Sep 8, 2025
9f83bb7
Migrate read models from Refund to Return
dysk Sep 8, 2025
684b459
Update and rename ordering tests after renaming refund to return.
dysk Sep 9, 2025
7b782de
Update subjects to ignore by Mutant after class rename.
dysk Sep 9, 2025
68da9b5
Follow rename from refund to return in rails app.
dysk Sep 9, 2025
4fa36e9
Don't use transformations for in-memory RES. Only apply for read old …
dysk Sep 9, 2025
3d38388
Mapper detects Hash vs Record and handles them correctly.
dysk Sep 9, 2025
4017014
Test mapper using RubyEventStore::Record instead of Hash objects.
dysk Sep 10, 2025
19a433a
Add default timestamp and valid_at values in case they are blank to p…
dysk Sep 10, 2025
d8cc0a0
Debug transformations.
dysk Sep 10, 2025
e0d6768
Temporarily disable transformations for the CI step to debug issues w…
dysk Sep 10, 2025
6a54275
Debug CI issues.
dysk Sep 10, 2025
40f74fb
Add PreserveTypes transformation to fix missing timestamp.
dysk Sep 10, 2025
a042479
Debug timestamp.
dysk Sep 10, 2025
a7d4fff
Remove DomainEvent transformation that causes blank timestamp issue.
dysk Sep 10, 2025
952552e
Remove debug env to fix mutation subjects preparation.
dysk Sep 11, 2025
644ccd4
Remove debug or make it not interfere with mutant.
dysk Sep 11, 2025
ba152c5
Remove debug code.
dysk Sep 11, 2025
d0d7f82
Remove debug leftovers.
dysk Sep 11, 2025
539d3b2
Add missing newlines at the end of files.
dysk Sep 11, 2025
2294fe6
Restore single command for db preparation.
dysk Sep 12, 2025
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
2 changes: 1 addition & 1 deletion ecommerce/ordering/.mutant.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ matcher:
- Ordering::Test*
- Ordering::Configuration#call
- Ordering::Configuration#initialize
- Ordering::RefundableProducts#call
- Ordering::ReturnableProducts#call
16 changes: 8 additions & 8 deletions ecommerce/ordering/lib/ordering.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
require "infra"
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/create_draft_refund"
require_relative "ordering/commands/add_item_to_refund"
require_relative "ordering/commands/remove_item_from_refund"
require_relative "ordering/events/draft_return_created"
require_relative "ordering/events/item_added_to_return"
require_relative "ordering/events/item_removed_from_return"
require_relative "ordering/commands/create_draft_return"
require_relative "ordering/commands/add_item_to_return"
require_relative "ordering/commands/remove_item_from_return"
require_relative "ordering/service"
require_relative "ordering/refund"
require_relative "ordering/refundable_products"
require_relative "ordering/return"
require_relative "ordering/returnable_products"

module Ordering
class Configuration
Expand Down
11 changes: 11 additions & 0 deletions ecommerce/ordering/lib/ordering/commands/add_item_to_return.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Ordering
class AddItemToReturn < Infra::Command
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID

alias aggregate_id return_id
end

AddItemToRefund = AddItemToReturn
end
10 changes: 10 additions & 0 deletions ecommerce/ordering/lib/ordering/commands/create_draft_return.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Ordering
class CreateDraftReturn < Infra::Command
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID

alias aggregate_id return_id
end

CreateDraftRefund = CreateDraftReturn
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Ordering
class RemoveItemFromReturn < Infra::Command
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID

alias aggregate_id return_id
end

RemoveItemFromRefund = RemoveItemFromReturn
end
14 changes: 14 additions & 0 deletions ecommerce/ordering/lib/ordering/events/draft_return_created.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Ordering
class DraftReturnCreated < Infra::Event
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :returnable_products, Infra::Types::Array.of(
Infra::Types::Hash.schema(
product_id: Infra::Types::UUID,
quantity: Infra::Types::Integer
)
)
end

DraftRefundCreated = DraftReturnCreated
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Ordering
class ItemAddedToReturn < Infra::Event
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
end

ItemAddedToRefund = ItemAddedToReturn
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Ordering
class ItemRemovedFromReturn < Infra::Event
attribute :return_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
end

ItemRemovedFromRefund = ItemRemovedFromReturn
end
75 changes: 75 additions & 0 deletions ecommerce/ordering/lib/ordering/return.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
module Ordering
class Return
include AggregateRoot

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

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

def create_draft(order_id, returnable_products)
apply DraftReturnCreated.new(data: { return_id: @id, order_id: order_id, returnable_products: returnable_products })
end

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

def remove_item(product_id)
raise ReturnHaveNotBeenRequestedForThisProductError unless @return_items.quantity(product_id).positive?
apply ItemRemovedFromReturn.new(data: { return_id: @id, order_id: @order_id, product_id: product_id })
end

on DraftReturnCreated do |event|
@order_id = event.data[:order_id]
@returnable_products = event.data[:returnable_products]
end

on ItemAddedToReturn do |event|
@return_items.increase_quantity(event.data[:product_id])
end

on ItemRemovedFromReturn do |event|
@return_items.decrease_quantity(event.data[:product_id])
end

private

def enough_items?(product_id)
@return_items.quantity(product_id) < returnable_quantity(product_id)
end

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

class ItemsList
attr_reader :return_items

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

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

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

def quantity(product_id)
return_items[product_id]
end
end

Refund = Return
RefundableProducts = ReturnableProducts if defined?(ReturnableProducts)
end
20 changes: 20 additions & 0 deletions ecommerce/ordering/lib/ordering/returnable_products.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Ordering
class ReturnableProducts
def call(event_store, order_id)
accepted_event = event_store
.read.backward
.stream("Pricing::Offer$#{order_id}")
.of_type(Pricing::OfferAccepted)
.first

placed_event = event_store
.read
.stream("Fulfillment::Order$#{order_id}")
.first

accepted_event.data.fetch(:order_lines) if placed_event
end
end

RefundableProducts = ReturnableProducts
end
28 changes: 16 additions & 12 deletions ecommerce/ordering/lib/ordering/service.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,51 @@
module Ordering
class OnCreateDraftRefund
class OnCreateDraftReturn
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(
@repository.with_aggregate(Return, command.aggregate_id) do |return_order|
return_order.create_draft(
command.order_id,
refundable_products(command.order_id)
returnable_products(command.order_id)
)
end
end

private

def refundable_products(order_id)
RefundableProducts.new.call(@event_store, order_id)
def returnable_products(order_id)
ReturnableProducts.new.call(@event_store, order_id)
end
end

class OnAddItemToRefund
class OnAddItemToReturn
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)
@repository.with_aggregate(Return, command.aggregate_id) do |return_order|
return_order.add_item(command.product_id)
end
end
end

class OnRemoveItemFromRefund
class OnRemoveItemFromReturn
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)
@repository.with_aggregate(Return, command.aggregate_id) do |return_order|
return_order.remove_item(command.product_id)
end
end
end

OnCreateDraftRefund = OnCreateDraftReturn
OnAddItemToRefund = OnAddItemToReturn
OnRemoveItemFromRefund = OnRemoveItemFromReturn
end
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
require_relative "test_helper"

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

def test_add_item_to_refund
def test_add_item_to_return
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}"
stream = "Ordering::Return$#{aggregate_id}"

arrange(
Pricing::SetPrice.new(product_id: product_1_id, price: 11),
Expand All @@ -22,18 +22,18 @@ def test_add_item_to_refund
Pricing::AddPriceItem.new(order_id: order_id, product_id: product_3_id, price: 33),
Pricing::AcceptOffer.new(order_id: order_id),
Fulfillment::RegisterOrder.new(order_id: order_id),
CreateDraftRefund.new(refund_id: aggregate_id, order_id: order_id),
AddItemToRefund.new(
refund_id: aggregate_id,
CreateDraftReturn.new(return_id: aggregate_id, order_id: order_id),
AddItemToReturn.new(
return_id: aggregate_id,
order_id: order_id,
product_id: product_2_id
)
)

expected_events = [
ItemAddedToRefund.new(
ItemAddedToReturn.new(
data: {
refund_id: aggregate_id,
return_id: aggregate_id,
order_id: order_id,
product_id: product_2_id
}
Expand All @@ -42,8 +42,8 @@ def test_add_item_to_refund

assert_events(stream, *expected_events) do
act(
AddItemToRefund.new(
refund_id: aggregate_id,
AddItemToReturn.new(
return_id: aggregate_id,
order_id: order_id,
product_id: product_2_id
)
Expand All @@ -61,16 +61,16 @@ def test_add_item_raises_exceeds_order_quantity_error
Pricing::AddPriceItem.new(order_id: order_id, product_id: product_id, price: 11),
Pricing::AcceptOffer.new(order_id: order_id),
Fulfillment::RegisterOrder.new(order_id: order_id),
CreateDraftRefund.new(refund_id: aggregate_id, order_id: order_id),
AddItemToRefund.new(
refund_id: aggregate_id,
CreateDraftReturn.new(return_id: aggregate_id, order_id: order_id),
AddItemToReturn.new(
return_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))
assert_raises(Ordering::Return::ExceedsOrderQuantityError) do
act(AddItemToReturn.new(return_id: aggregate_id, order_id: order_id, product_id: product_id))
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
require_relative "test_helper"

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

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

arrange(
Pricing::SetPrice.new(product_id: product_id, price: 11),
Expand All @@ -19,19 +19,19 @@ def test_draft_refund_created
)

expected_events = [
DraftRefundCreated.new(
DraftReturnCreated.new(
data: {
refund_id: aggregate_id,
return_id: aggregate_id,
order_id: order_id,
refundable_products: [{ product_id:, quantity: 2 }]
returnable_products: [{ product_id:, quantity: 2 }]
}
)
]

assert_events(stream, *expected_events) do
act(
CreateDraftRefund.new(
refund_id: aggregate_id,
CreateDraftReturn.new(
return_id: aggregate_id,
order_id: order_id
)
)
Expand Down
Loading