diff --git a/ecommerce/taxes/.mutant.yml b/ecommerce/taxes/.mutant.yml index 67b7b1f8..f0354b5f 100644 --- a/ecommerce/taxes/.mutant.yml +++ b/ecommerce/taxes/.mutant.yml @@ -10,4 +10,6 @@ matcher: ignore: - Taxes::Test* - Taxes::Configuration* - - Taxes::VatRateCatalog#vat_rate_for \ No newline at end of file + - Taxes::VatRateCatalog#vat_rate_for + - Taxes::AddAvailableVatRateHandler* + - Taxes::RemoveAvailableVatRateHandler* diff --git a/ecommerce/taxes/lib/taxes.rb b/ecommerce/taxes/lib/taxes.rb index d18aa918..db2a0d70 100644 --- a/ecommerce/taxes/lib/taxes.rb +++ b/ecommerce/taxes/lib/taxes.rb @@ -11,6 +11,7 @@ def call(event_store, command_bus) command_bus.register(SetVatRate, SetVatRateHandler.new(event_store)) command_bus.register(DetermineVatRate, DetermineVatRateHandler.new(event_store)) command_bus.register(AddAvailableVatRate, AddAvailableVatRateHandler.new(event_store)) + command_bus.register(RemoveAvailableVatRate, RemoveAvailableVatRateHandler.new(event_store)) end end end diff --git a/ecommerce/taxes/lib/taxes/commands.rb b/ecommerce/taxes/lib/taxes/commands.rb index 0e958579..b100488c 100644 --- a/ecommerce/taxes/lib/taxes/commands.rb +++ b/ecommerce/taxes/lib/taxes/commands.rb @@ -13,4 +13,8 @@ class AddAvailableVatRate < Infra::Command attribute :available_vat_rate_id, Infra::Types::UUID attribute :vat_rate, Infra::Types::VatRate end + + class RemoveAvailableVatRate < Infra::Command + attribute :vat_rate_code, Infra::Types::String + end end diff --git a/ecommerce/taxes/lib/taxes/events.rb b/ecommerce/taxes/lib/taxes/events.rb index e92c1a11..1e3b1906 100644 --- a/ecommerce/taxes/lib/taxes/events.rb +++ b/ecommerce/taxes/lib/taxes/events.rb @@ -14,4 +14,8 @@ class AvailableVatRateAdded < Infra::Event attribute :available_vat_rate_id, Infra::Types::UUID attribute :vat_rate, Infra::Types::VatRate end + + class AvailableVatRateRemoved < Infra::Event + attribute :vat_rate_code, Infra::Types::String + end end diff --git a/ecommerce/taxes/lib/taxes/services.rb b/ecommerce/taxes/lib/taxes/services.rb index 97c4d0b9..35713bbc 100644 --- a/ecommerce/taxes/lib/taxes/services.rb +++ b/ecommerce/taxes/lib/taxes/services.rb @@ -1,6 +1,7 @@ module Taxes VatRateAlreadyExists = Class.new(StandardError) VatRateNotApplicable = Class.new(StandardError) + VatRateNotExists = Class.new(StandardError) class SetVatRateHandler def initialize(event_store) @repository = Infra::AggregateRootRepository.new(event_store) @@ -41,20 +42,32 @@ def stream_name(order_id) end class AddAvailableVatRateHandler + include Infra::Retry + 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) + with_retry do + event = last_available_vat_rate_event(cmd.vat_rate.code) + raise VatRateAlreadyExists if event.instance_of?(AvailableVatRateAdded) - event_store.publish(available_vat_rate_added_event(cmd), stream_name: stream_name(cmd)) + expected_version = event ? event_store.position_in_stream(event.event_id, stream_name(cmd)) : -1 + event_store.publish(available_vat_rate_added_event(cmd), stream_name: stream_name(cmd), expected_version: expected_version) + end end private - attr_reader :event_store, :catalog + attr_reader :event_store + + def last_available_vat_rate_event(vat_rate_code) + event_store + .read + .stream("Taxes::AvailableVatRate$#{vat_rate_code}") + .last + end def available_vat_rate_added_event(cmd) AvailableVatRateAdded.new( @@ -69,4 +82,44 @@ def stream_name(cmd) "Taxes::AvailableVatRate$#{cmd.vat_rate.code}" end end + + class RemoveAvailableVatRateHandler + include Infra::Retry + + def initialize(event_store) + @event_store = event_store + end + + def call(cmd) + with_retry do + event = last_available_vat_rate_event(cmd.vat_rate_code) + raise VatRateNotExists unless event.instance_of?(AvailableVatRateAdded) + + event_store.publish( + available_vat_rate_removed_event(cmd), + stream_name: stream_name(cmd), + expected_version: event_store.position_in_stream(event.event_id, stream_name(cmd)) + ) + end + end + + private + + attr_reader :event_store + + def last_available_vat_rate_event(vat_rate_code) + event_store + .read + .stream("Taxes::AvailableVatRate$#{vat_rate_code}") + .last + end + + def available_vat_rate_removed_event(cmd) + AvailableVatRateRemoved.new(data: { vat_rate_code: cmd.vat_rate_code }) + 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 98f5de33..5acbab6f 100644 --- a/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb +++ b/ecommerce/taxes/lib/taxes/vat_rate_catalog.rb @@ -16,13 +16,17 @@ def vat_rate_for(product_id) end def vat_rate_by_code(vat_rate_code) - @event_store + last_available_vat_rate_event = @event_store .read .stream("Taxes::AvailableVatRate$#{vat_rate_code}") .last - &.data - &.fetch(:vat_rate) - &.then { |vat_rate| Infra::Types::VatRate.new(vat_rate) } + + if last_available_vat_rate_event.instance_of?(AvailableVatRateAdded) + last_available_vat_rate_event + .data + .fetch(:vat_rate) + .then { |vat_rate| Infra::Types::VatRate.new(vat_rate) } + end end end end diff --git a/ecommerce/taxes/test/taxes_test.rb b/ecommerce/taxes/test/taxes_test.rb index ca61b806..c902d12d 100644 --- a/ecommerce/taxes/test/taxes_test.rb +++ b/ecommerce/taxes/test/taxes_test.rb @@ -59,6 +59,22 @@ def test_should_not_allow_for_double_registration end end + def test_removing_available_vat_rate + vat_rate = Infra::Types::VatRate.new(code: "50", rate: 50) + add_available_vat_rate(vat_rate) + available_vat_rate_removed = AvailableVatRateRemoved.new(data: { vat_rate_code: vat_rate.code }) + + assert_events("Taxes::AvailableVatRate$#{vat_rate.code}", available_vat_rate_removed) do + remove_available_vat_rate(vat_rate.code) + end + end + + def test_cannot_remove_non_existing_vat_rate + assert_raises(VatRateNotExists) do + remove_available_vat_rate("50") + end + end + private def set_vat_rate(product_id, vat_rate_code) @@ -72,5 +88,9 @@ def determine_vat_rate(order_id, product_id, vat_rate) 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 + + def remove_available_vat_rate(vat_rate_code) + run_command(RemoveAvailableVatRate.new(vat_rate_code: vat_rate_code)) + end end end diff --git a/ecommerce/taxes/test/vat_rate_catalog_test.rb b/ecommerce/taxes/test/vat_rate_catalog_test.rb index d253925c..c8d27fab 100644 --- a/ecommerce/taxes/test/vat_rate_catalog_test.rb +++ b/ecommerce/taxes/test/vat_rate_catalog_test.rb @@ -15,6 +15,12 @@ def test_returns_available_vat_rate def test_returns_nil_when_vat_rate_is_not_available assert_nil catalog.vat_rate_by_code("60") end + + def test_returns_nil_when_vat_rate_was_removed + run_command(RemoveAvailableVatRate.new(vat_rate_code: "50")) + + assert_nil catalog.vat_rate_by_code("50") + end end private diff --git a/rails_application/app/controllers/available_vat_rates_controller.rb b/rails_application/app/controllers/available_vat_rates_controller.rb index 783461bd..888a389f 100644 --- a/rails_application/app/controllers/available_vat_rates_controller.rb +++ b/rails_application/app/controllers/available_vat_rates_controller.rb @@ -26,10 +26,10 @@ def create 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 + rescue Taxes::VatRateAlreadyExists + flash.now[:alert] = "VAT rate already exists" + render "new", status: :unprocessable_entity + else redirect_to available_vat_rates_path, notice: "VAT rate was successfully created" end @@ -37,6 +37,13 @@ def index @available_vat_rates = VatRates::AvailableVatRate.all end + def destroy + remove_available_vat_rate(params[:vat_rate_code]) + redirect_to available_vat_rates_path, notice: "VAT rate was successfully removed" + rescue Taxes::VatRateNotExists + redirect_to available_vat_rates_path, alert: "VAT rate does not exist" + end + private def add_available_vat_rate(code, rate, available_vat_rate_id) @@ -50,6 +57,14 @@ def add_available_vat_rate_cmd(code, rate, available_vat_rate_id) ) end + def remove_available_vat_rate(vat_rate_code) + command_bus.(remove_available_vat_rate_cmd(vat_rate_code)) + end + + def remove_available_vat_rate_cmd(vat_rate_code) + Taxes::RemoveAvailableVatRate.new(vat_rate_code: vat_rate_code) + end + def available_vat_rate_params params.permit(:code, :rate) end diff --git a/rails_application/app/read_models/vat_rates/configuration.rb b/rails_application/app/read_models/vat_rates/configuration.rb index 1d41b3d0..1a29d673 100644 --- a/rails_application/app/read_models/vat_rates/configuration.rb +++ b/rails_application/app/read_models/vat_rates/configuration.rb @@ -6,6 +6,7 @@ class AvailableVatRate < ApplicationRecord class Configuration def call(event_store) event_store.subscribe(AddAvailableVatRate, to: [Taxes::AvailableVatRateAdded]) + event_store.subscribe(RemoveAvailableVatRate, to: [Taxes::AvailableVatRateRemoved]) end end end diff --git a/rails_application/app/read_models/vat_rates/remove_available_vat_rate.rb b/rails_application/app/read_models/vat_rates/remove_available_vat_rate.rb new file mode 100644 index 00000000..ebee0b19 --- /dev/null +++ b/rails_application/app/read_models/vat_rates/remove_available_vat_rate.rb @@ -0,0 +1,7 @@ +module VatRates + class RemoveAvailableVatRate + def call(event) + AvailableVatRate.destroy_by(code: event.data.fetch(:vat_rate_code)) + end + end +end diff --git a/rails_application/app/views/available_vat_rates/index.html.erb b/rails_application/app/views/available_vat_rates/index.html.erb index 3632b7e7..37cb7f12 100644 --- a/rails_application/app/views/available_vat_rates/index.html.erb +++ b/rails_application/app/views/available_vat_rates/index.html.erb @@ -13,6 +13,7 @@ Code Rate + @@ -20,7 +21,12 @@ <% @available_vat_rates.each do |available_vat_rate| %> <%= available_vat_rate.code %> - <%= available_vat_rate.rate %> + <%= available_vat_rate.rate %> + + <%= form_with url: available_vat_rates_path, method: :delete do |f| %> + <%= f.hidden_field :vat_rate_code, value: available_vat_rate.code %> + <%= f.submit 'Delete' %> + <% end %> <% end %> diff --git a/rails_application/config/routes.rb b/rails_application/config/routes.rb index 88ed31ad..f68854b9 100644 --- a/rails_application/config/routes.rb +++ b/rails_application/config/routes.rb @@ -47,6 +47,7 @@ end resources :available_vat_rates, only: [:new, :create, :index] + delete "/available_vat_rates", to: "available_vat_rates#destroy" post :login, to: "client/clients#login" get :logout, to: "client/clients#logout" diff --git a/rails_application/test/integration/available_vat_rates_test.rb b/rails_application/test/integration/available_vat_rates_test.rb index 20ae6fed..de444716 100644 --- a/rails_application/test/integration/available_vat_rates_test.rb +++ b/rails_application/test/integration/available_vat_rates_test.rb @@ -15,6 +15,14 @@ def test_happy_path assert_equal "VAT rate was successfully created", flash[:notice] assert_select "h1", "VAT Rates" + + delete "/available_vat_rates", + params: { + "vat_rate_code" => "10.0" + } + follow_redirect! + + assert_equal "VAT rate was successfully removed", flash[:notice] end def test_validation_blank_errors @@ -58,6 +66,6 @@ def test_vat_rate_already_exists } assert_response :unprocessable_entity - assert_select "#notice", "VAT rate already exists" + assert_select "#alert", "VAT rate already exists" end end