diff --git a/ecommerce/configuration.rb b/ecommerce/configuration.rb index 2af4d70bf..06caa161b 100644 --- a/ecommerce/configuration.rb +++ b/ecommerce/configuration.rb @@ -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) @@ -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) } diff --git a/ecommerce/taxes/lib/taxes.rb b/ecommerce/taxes/lib/taxes.rb index f1c9db7a7..d18aa918d 100644 --- a/ecommerce/taxes/lib/taxes.rb +++ b/ecommerce/taxes/lib/taxes.rb @@ -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 diff --git a/ecommerce/taxes/lib/taxes/commands.rb b/ecommerce/taxes/lib/taxes/commands.rb index b3bb5153a..0e958579a 100644 --- a/ecommerce/taxes/lib/taxes/commands.rb +++ b/ecommerce/taxes/lib/taxes/commands.rb @@ -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 \ No newline at end of file + + class AddAvailableVatRate < Infra::Command + attribute :available_vat_rate_id, Infra::Types::UUID + attribute :vat_rate, Infra::Types::VatRate + end +end diff --git a/ecommerce/taxes/lib/taxes/events.rb b/ecommerce/taxes/lib/taxes/events.rb index 7f3f308b9..e92c1a117 100644 --- a/ecommerce/taxes/lib/taxes/events.rb +++ b/ecommerce/taxes/lib/taxes/events.rb @@ -9,4 +9,9 @@ class VatRateDetermined < Infra::Event attribute :product_id, Infra::Types::UUID attribute :vat_rate, Infra::Types::VatRate end -end \ No newline at end of file + + class AvailableVatRateAdded < Infra::Event + attribute :available_vat_rate_id, Infra::Types::UUID + attribute :vat_rate, Infra::Types::VatRate + end +end diff --git a/ecommerce/taxes/lib/taxes/product.rb b/ecommerce/taxes/lib/taxes/product.rb index 493c1825c..8fe6c027e 100644 --- a/ecommerce/taxes/lib/taxes/product.rb +++ b/ecommerce/taxes/lib/taxes/product.rb @@ -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 diff --git a/ecommerce/taxes/lib/taxes/services.rb b/ecommerce/taxes/lib/taxes/services.rb index eb639f8bf..97c4d0b90 100644 --- a/ecommerce/taxes/lib/taxes/services.rb +++ b/ecommerce/taxes/lib/taxes/services.rb @@ -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 @@ -34,4 +39,34 @@ def stream_name(order_id) "Taxes::Order$#{order_id}" end end -end \ No newline at end of file + + 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}" + end + end +end diff --git a/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb b/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb index d00fd4266..98f5de33b 100644 --- a/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb +++ b/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb @@ -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 \ No newline at end of file +end diff --git a/ecommerce/taxes/test/taxes_test.rb b/ecommerce/taxes/test/taxes_test.rb index 77e58d57f..ca61b8064 100644 --- a/ecommerce/taxes/test/taxes_test.rb +++ b/ecommerce/taxes/test/taxes_test.rb @@ -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 }) 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 diff --git a/ecommerce/taxes/test/test_helper.rb b/ecommerce/taxes/test/test_helper.rb index a23c2d113..6a1f5333e 100644 --- a/ecommerce/taxes/test/test_helper.rb +++ b/ecommerce/taxes/test/test_helper.rb @@ -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 diff --git a/ecommerce/taxes/test/vat_rate_catalog_test.rb b/ecommerce/taxes/test/vat_rate_catalog_test.rb new file mode 100644 index 000000000..d253925c7 --- /dev/null +++ b/ecommerce/taxes/test/vat_rate_catalog_test.rb @@ -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 diff --git a/rails_application/app/controllers/available_vat_rates_controller.rb b/rails_application/app/controllers/available_vat_rates_controller.rb new file mode 100644 index 000000000..34f3debf5 --- /dev/null +++ b/rails_application/app/controllers/available_vat_rates_controller.rb @@ -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 + + 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 diff --git a/rails_application/app/controllers/products_controller.rb b/rails_application/app/controllers/products_controller.rb index 87b0e14ca..08498a691 100644 --- a/rails_application/app/controllers/products_controller.rb +++ b/rails_application/app/controllers/products_controller.rb @@ -46,15 +46,15 @@ def create if product_form.vat_rate.present? set_product_vat_rate(product_form.product_id, product_form.vat_rate) end - rescue ProductCatalog::AlreadyRegistered - flash[:notice] = "Product was already registered" - render "new" - rescue Taxes::Product::VatRateNotApplicable - flash[:notice] = "Selected VAT rate not applicable" - render "new" - else - redirect_to products_path, notice: "Product was successfully created" end + + redirect_to products_path, notice: "Product was successfully created" + rescue ProductCatalog::AlreadyRegistered + flash[:notice] = "Product was already registered" + render "new" + rescue Taxes::VatRateNotApplicable + flash[:notice] = "Selected VAT rate is not applicable" + render "new" end def update @@ -91,8 +91,7 @@ def set_future_product_price(product_id, price, valid_since) command_bus.(set_product_future_price_cmd(product_id, price, valid_since)) end - def set_product_vat_rate(product_id, vat_rate_code) - vat_rate = Taxes::Configuration.available_vat_rates.find{|rate| rate.code == vat_rate_code} + def set_product_vat_rate(product_id, vat_rate) command_bus.(set_product_vat_rate_cmd(product_id, vat_rate)) end @@ -113,7 +112,7 @@ def set_product_price_cmd(product_id, price) end def set_product_vat_rate_cmd(product_id, vat_rate) - Taxes::SetVatRate.new(product_id: product_id, vat_rate: vat_rate) + Taxes::SetVatRate.new(product_id: product_id, vat_rate_code: vat_rate) end def set_product_future_price_cmd(product_id, price, valid_since) diff --git a/rails_application/app/read_models/vat_rates/add_available_vat_rate.rb b/rails_application/app/read_models/vat_rates/add_available_vat_rate.rb new file mode 100644 index 000000000..5f418bb14 --- /dev/null +++ b/rails_application/app/read_models/vat_rates/add_available_vat_rate.rb @@ -0,0 +1,11 @@ +module VatRates + class AddAvailableVatRate + def call(event) + AvailableVatRate.create!( + uid: event.data.fetch(:available_vat_rate_id), + code: event.data.fetch(:vat_rate).fetch(:code), + rate: event.data.fetch(:vat_rate).fetch(:rate) + ) + end + end +end diff --git a/rails_application/app/read_models/vat_rates/configuration.rb b/rails_application/app/read_models/vat_rates/configuration.rb new file mode 100644 index 000000000..1d41b3d02 --- /dev/null +++ b/rails_application/app/read_models/vat_rates/configuration.rb @@ -0,0 +1,11 @@ +module VatRates + class AvailableVatRate < ApplicationRecord + self.table_name = "available_vat_rates" + end + + class Configuration + def call(event_store) + event_store.subscribe(AddAvailableVatRate, to: [Taxes::AvailableVatRateAdded]) + end + end +end diff --git a/rails_application/app/views/application/_top_navigation.html.erb b/rails_application/app/views/application/_top_navigation.html.erb index 16a1b7ead..0f2dd6a75 100644 --- a/rails_application/app/views/application/_top_navigation.html.erb +++ b/rails_application/app/views/application/_top_navigation.html.erb @@ -15,6 +15,7 @@ <%= navigation_link "Coupons", coupons_path %> <%= navigation_link "Time Promotions", time_promotions_path %> <%= navigation_link "Customers", customers_path %> + <%= navigation_link "VAT Rates", available_vat_rates_path %> <%= navigation_link "Client View", clients_path %> diff --git a/rails_application/app/views/available_vat_rates/index.html.erb b/rails_application/app/views/available_vat_rates/index.html.erb new file mode 100644 index 000000000..3632b7e76 --- /dev/null +++ b/rails_application/app/views/available_vat_rates/index.html.erb @@ -0,0 +1,27 @@ +<% content_for(:header) do %> + VAT Rates +<% end %> + +<% content_for(:actions) do %> + <%= primary_action_button do %> + <%= link_to 'New VAT Rate', new_available_vat_rate_path %> + <% end %> +<% end %> + +
Code | +Rate | +
---|---|
<%= available_vat_rate.code %> | +<%= available_vat_rate.rate %> | +