Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow creating custom vat rates #377

Merged
merged 4 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 2 additions & 3 deletions ecommerce/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@

module Ecommerce
class Configuration
def initialize(number_generator: nil, payment_gateway: nil, available_vat_rates: [])
def initialize(number_generator: nil, payment_gateway: nil)
@number_generator = number_generator
@payment_gateway = payment_gateway
@available_vat_rates = available_vat_rates
end

def call(event_store, command_bus)
Expand All @@ -37,7 +36,7 @@ def configure_bounded_contexts(event_store, command_bus)
Payments::Configuration.new(@payment_gateway),
Shipping::Configuration.new,
Pricing::Configuration.new,
Taxes::Configuration.new(@available_vat_rates),
Taxes::Configuration.new,
ProductCatalog::Configuration.new,
Fulfillment::Configuration.new
].each { |c| c.call(event_store, command_bus) }
Expand Down
10 changes: 1 addition & 9 deletions ecommerce/taxes/lib/taxes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@

module Taxes
class Configuration
def self.available_vat_rates
@@available_vat_rates
end

def initialize(available_vat_rates = [])
@available_vat_rates = available_vat_rates
end

def call(event_store, command_bus)
@@available_vat_rates = @available_vat_rates
command_bus.register(SetVatRate, SetVatRateHandler.new(event_store))
command_bus.register(DetermineVatRate, DetermineVatRateHandler.new(event_store))
command_bus.register(AddAvailableVatRate, AddAvailableVatRateHandler.new(event_store))
end
end
end
9 changes: 7 additions & 2 deletions ecommerce/taxes/lib/taxes/commands.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
module Taxes
class SetVatRate < Infra::Command
attribute :product_id, Infra::Types::UUID
attribute :vat_rate, Infra::Types::VatRate
attribute :vat_rate_code, Infra::Types::String
end

class DetermineVatRate < Infra::Command
attribute :product_id, Infra::Types::UUID
attribute :order_id, Infra::Types::UUID
end
end

class AddAvailableVatRate < Infra::Command
attribute :available_vat_rate_id, Infra::Types::UUID
attribute :vat_rate, Infra::Types::VatRate
end
end
7 changes: 6 additions & 1 deletion ecommerce/taxes/lib/taxes/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ class VatRateDetermined < Infra::Event
attribute :product_id, Infra::Types::UUID
attribute :vat_rate, Infra::Types::VatRate
end
end

class AvailableVatRateAdded < Infra::Event
attribute :available_vat_rate_id, Infra::Types::UUID
attribute :vat_rate, Infra::Types::VatRate
end
end
7 changes: 0 additions & 7 deletions ecommerce/taxes/lib/taxes/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,16 @@ module Taxes
class Product
include AggregateRoot

VatRateNotApplicable = Class.new(StandardError)

def initialize(id)
@id = id
end

def set_vat_rate(vat_rate)
raise VatRateNotApplicable unless vat_rate_applicable?(vat_rate)
apply(VatRateSet.new(data: { product_id: @id, vat_rate: vat_rate }))
end

private

def vat_rate_applicable?(vat_rate)
Configuration.available_vat_rates.include?(vat_rate)
end

on(VatRateSet) { |_| }
end
end
39 changes: 37 additions & 2 deletions ecommerce/taxes/lib/taxes/services.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
module Taxes
VatRateAlreadyExists = Class.new(StandardError)
VatRateNotApplicable = Class.new(StandardError)
class SetVatRateHandler
def initialize(event_store)
@repository = Infra::AggregateRootRepository.new(event_store)
@catalog = VatRateCatalog.new(event_store)
end

def call(cmd)
vat_rate = @catalog.vat_rate_by_code(cmd.vat_rate_code)
raise VatRateNotApplicable unless vat_rate
@repository.with_aggregate(Product, cmd.product_id) do |product|
product.set_vat_rate(cmd.vat_rate)
product.set_vat_rate(vat_rate)
end
end
end
Expand Down Expand Up @@ -34,4 +39,34 @@ def stream_name(order_id)
"Taxes::Order$#{order_id}"
end
end
end

class AddAvailableVatRateHandler
def initialize(event_store)
@catalog = VatRateCatalog.new(event_store)
@event_store = event_store
end

def call(cmd)
raise VatRateAlreadyExists if catalog.vat_rate_by_code(cmd.vat_rate.code)

event_store.publish(available_vat_rate_added_event(cmd), stream_name: stream_name(cmd))
end

private

attr_reader :event_store, :catalog

def available_vat_rate_added_event(cmd)
AvailableVatRateAdded.new(
data: {
available_vat_rate_id: cmd.available_vat_rate_id,
vat_rate: cmd.vat_rate
}
)
end

def stream_name(cmd)
"Taxes::AvailableVatRate$#{cmd.vat_rate.code}"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using code instead of uuid in the stream name.
This helps ensure uniqueness of vat rates and simplifies some logic.

Please let me know what you think.

Copy link
Collaborator

@lukaszreszke lukaszreszke Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it is a good choice. However I am wondering if the model is correct. But I don't have an answer yet. It is good for first iteration I guess.

end
end
end
12 changes: 11 additions & 1 deletion ecommerce/taxes/lib/taxes/vat_rate_catalog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,15 @@ def vat_rate_for(product_id)
&.data
&.fetch(:vat_rate)
end

def vat_rate_by_code(vat_rate_code)
@event_store
.read
.stream("Taxes::AvailableVatRate$#{vat_rate_code}")
.last
&.data
&.fetch(:vat_rate)
&.then { |vat_rate| Infra::Types::VatRate.new(vat_rate) }
end
end
end
end
55 changes: 39 additions & 16 deletions ecommerce/taxes/test/taxes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,74 @@
module Taxes
class TaxesTest < Test
def test_setting_available_vat_rate
vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50)
add_available_vat_rate(vat_rate)

product_id = SecureRandom.uuid
vat_rate_set = VatRateSet.new(data: { product_id: product_id, vat_rate: available_vat_rate })
vat_rate_set = VatRateSet.new(data: { product_id: product_id, vat_rate: vat_rate })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small tip, you could do

vat_rate_set = VatRateSet.new(data: { product_id:, vat_rate: })

instead :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to leave this as is for now, as it is consistent with the rest of the code in the project. In the future we can add a rubocop rule to enforce style guides :)

assert_events("Taxes::Product$#{product_id}", vat_rate_set) do
set_vat_rate(product_id, available_vat_rate)
set_vat_rate(product_id, vat_rate.code)
end
end

def test_setting_unavailable_vat_rate_should_raise_error
product_id = SecureRandom.uuid
assert_raises(Product::VatRateNotApplicable) do
set_vat_rate(product_id, unavailable_vat_rate)
unavailable_vat_rate = Infra::Types::VatRate.new(code: "20", rate: 20)

assert_raises(Taxes::VatRateNotApplicable) do
set_vat_rate(product_id, unavailable_vat_rate.code)
end
end

def test_determining_vat_rate
vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50)
add_available_vat_rate(vat_rate)

order_id = SecureRandom.uuid
product_id = SecureRandom.uuid
another_product_id = SecureRandom.uuid

set_vat_rate(product_id, available_vat_rate)
vat_rate_determined = VatRateDetermined.new(data: { order_id: order_id, product_id: product_id, vat_rate: available_vat_rate })
set_vat_rate(product_id, vat_rate.code)
vat_rate_determined = VatRateDetermined.new(data: { order_id: order_id, product_id: product_id, vat_rate: vat_rate })
assert_events("Taxes::Order$#{order_id}", vat_rate_determined) do
determine_vat_rate(order_id, product_id, available_vat_rate)
determine_vat_rate(order_id, product_id, vat_rate)
end
assert_events("Taxes::Order$#{order_id}") do
determine_vat_rate(order_id, another_product_id, available_vat_rate)
determine_vat_rate(order_id, another_product_id, vat_rate)
end
end

def test_adding_available_vat_rate
available_vat_rate_id = SecureRandom.uuid
vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50)
available_vat_rate_added = AvailableVatRateAdded.new(data: { available_vat_rate_id: available_vat_rate_id, vat_rate: vat_rate })

assert_events("Taxes::AvailableVatRate$#{vat_rate.code}", available_vat_rate_added) do
add_available_vat_rate(vat_rate, available_vat_rate_id)
end
end

def test_should_not_allow_for_double_registration
vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50)
add_available_vat_rate(vat_rate)

assert_raises(VatRateAlreadyExists) do
add_available_vat_rate(vat_rate)
end
end

private

def set_vat_rate(product_id, vat_rate)
run_command(SetVatRate.new(product_id: product_id, vat_rate: vat_rate))
def set_vat_rate(product_id, vat_rate_code)
run_command(SetVatRate.new(product_id: product_id, vat_rate_code: vat_rate_code))
end

def determine_vat_rate(order_id, product_id, vat_rate)
run_command(DetermineVatRate.new(order_id: order_id, product_id: product_id, vat_rate: vat_rate))
end

def available_vat_rate
Configuration.available_vat_rates.first
end

def unavailable_vat_rate
Infra::Types::VatRate.new(code: "50", rate: 50)
def add_available_vat_rate(vat_rate, available_vat_rate_id = SecureRandom.uuid)
run_command(AddAvailableVatRate.new(available_vat_rate_id: available_vat_rate_id, vat_rate: vat_rate))
end
end
end
8 changes: 1 addition & 7 deletions ecommerce/taxes/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ class Test < Infra::InMemoryTest

def before_setup
super
Configuration.new([dummy_vat_rate]).call(event_store, command_bus)
end

private

def dummy_vat_rate
Infra::Types::VatRate.new(code: "20", rate: 20)
Configuration.new.call(event_store, command_bus)
end
end
end
30 changes: 30 additions & 0 deletions ecommerce/taxes/test/vat_rate_catalog_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require_relative "test_helper"

module Taxes
class VatRateCatalogTest < Test
class VatRateByCodeTest < VatRateCatalogTest
def setup
@vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50)
add_available_vat_rate(@vat_rate)
end

def test_returns_available_vat_rate
assert_equal @vat_rate, catalog.vat_rate_by_code("50")
end

def test_returns_nil_when_vat_rate_is_not_available
assert_nil catalog.vat_rate_by_code("60")
end
end

private

def catalog
VatRateCatalog.new(@event_store)
end

def add_available_vat_rate(vat_rate, available_vat_rate_id = SecureRandom.uuid)
run_command(AddAvailableVatRate.new(available_vat_rate_id: available_vat_rate_id, vat_rate: vat_rate))
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class AvailableVatRatesController < ApplicationController
class AvailableVatRateForm
include ActiveModel::Model
include ActiveModel::Validations

attr_reader :code, :rate

def initialize(params)
@code = params[:code]
@rate = params[:rate]
end
Comment on lines +6 to +11
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


validates :code, presence: true
validates :rate, presence: true, numericality: { only_numeric: true, greater_than: 0 }
end

def new
end

def create
available_vat_rate_id = SecureRandom.uuid
available_vat_rate_form = AvailableVatRateForm.new(available_vat_rate_params)

unless available_vat_rate_form.valid?
return render "new", locals: { errors: available_vat_rate_form.errors }, status: :unprocessable_entity
end

add_available_vat_rate(available_vat_rate_form.code, available_vat_rate_form.rate, available_vat_rate_id)
rescue Taxes::VatRateAlreadyExists
flash.now[:notice] = "VAT rate already exists"
render "new", status: :unprocessable_entity
else
redirect_to available_vat_rates_path, notice: "VAT rate was successfully created"
end

def index
@available_vat_rates = VatRates::AvailableVatRate.all
end

private

def add_available_vat_rate(code, rate, available_vat_rate_id)
command_bus.(add_available_vat_rate_cmd(code, rate, available_vat_rate_id))
end

def add_available_vat_rate_cmd(code, rate, available_vat_rate_id)
Taxes::AddAvailableVatRate.new(
available_vat_rate_id: available_vat_rate_id,
vat_rate: Infra::Types::VatRate.new(code: code, rate: rate)
)
end

def available_vat_rate_params
params.permit(:code, :rate)
end
end
Loading