From d920775a2a2b646344bf707ac0112b02141b5c52 Mon Sep 17 00:00:00 2001
From: Marlena Borowiec <96994176+marlena-b@users.noreply.github.com>
Date: Mon, 19 Aug 2024 21:00:50 +0200
Subject: [PATCH 1/2] Revert "Revert "Allow creating custom vat rates""

---
 ecommerce/configuration.rb                    |  5 +-
 ecommerce/taxes/lib/taxes.rb                  | 10 +---
 ecommerce/taxes/lib/taxes/commands.rb         |  9 ++-
 ecommerce/taxes/lib/taxes/events.rb           |  7 ++-
 ecommerce/taxes/lib/taxes/product.rb          |  7 ---
 ecommerce/taxes/lib/taxes/services.rb         | 39 ++++++++++++-
 ecommerce/taxes/lib/taxes/vat_rate_catalog.rb | 12 +++-
 ecommerce/taxes/test/taxes_test.rb            | 55 ++++++++++++------
 ecommerce/taxes/test/test_helper.rb           |  8 +--
 ecommerce/taxes/test/vat_rate_catalog_test.rb | 30 ++++++++++
 .../available_vat_rates_controller.rb         | 56 +++++++++++++++++++
 .../app/controllers/products_controller.rb    | 21 ++++---
 .../vat_rates/add_available_vat_rate.rb       | 11 ++++
 .../read_models/vat_rates/configuration.rb    | 11 ++++
 .../application/_top_navigation.html.erb      |  1 +
 .../views/available_vat_rates/index.html.erb  | 27 +++++++++
 .../views/available_vat_rates/new.html.erb    | 37 ++++++++++++
 .../app/views/products/new.html.erb           |  4 +-
 rails_application/config/routes.rb            |  3 +
 ...240812080357_create_available_vat_rates.rb | 11 ++++
 rails_application/db/schema.rb                | 10 +++-
 rails_application/db/seeds.rb                 | 14 ++++-
 rails_application/lib/configuration.rb        | 11 ++--
 .../integration/available_vat_rates_test.rb   | 52 +++++++++++++++++
 .../test/integration/client_orders_test.rb    |  1 +
 .../test/integration/customers_test.rb        |  5 ++
 .../test/integration/discount_test.rb         |  2 +-
 .../test/integration/orders_test.rb           |  1 +
 .../test/integration/products_test.rb         |  1 +
 .../test/integration/public_offer_test.rb     |  1 +
 .../test/invoices/invoices_test.rb            | 19 ++++---
 .../test/read_model_handler_test.rb           |  2 +-
 rails_application/test/test_helper.rb         |  4 ++
 .../available_vat_rate_added_test.rb          | 39 +++++++++++++
 34 files changed, 447 insertions(+), 79 deletions(-)
 create mode 100644 ecommerce/taxes/test/vat_rate_catalog_test.rb
 create mode 100644 rails_application/app/controllers/available_vat_rates_controller.rb
 create mode 100644 rails_application/app/read_models/vat_rates/add_available_vat_rate.rb
 create mode 100644 rails_application/app/read_models/vat_rates/configuration.rb
 create mode 100644 rails_application/app/views/available_vat_rates/index.html.erb
 create mode 100644 rails_application/app/views/available_vat_rates/new.html.erb
 create mode 100644 rails_application/db/migrate/20240812080357_create_available_vat_rates.rb
 create mode 100644 rails_application/test/integration/available_vat_rates_test.rb
 create mode 100644 rails_application/test/vat_rates/available_vat_rate_added_test.rb

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 %>
           </div>
         </div>
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 %>
+
+<table class="w-full">
+  <thead>
+    <tr>
+      <th class="text-left py-2">Code</th>
+      <th class="text-right py-2">Rate</th>
+    </tr>
+  </thead>
+
+  <tbody>
+  <% @available_vat_rates.each do |available_vat_rate| %>
+    <tr class="border-t">
+      <td class="py-2"><%= available_vat_rate.code %></td>
+      <td class="py-2 text-right"><%= available_vat_rate.rate %></td>
+    </tr>
+  <% end %>
+  </tbody>
+</table>
diff --git a/rails_application/app/views/available_vat_rates/new.html.erb b/rails_application/app/views/available_vat_rates/new.html.erb
new file mode 100644
index 000000000..d5e72a8fe
--- /dev/null
+++ b/rails_application/app/views/available_vat_rates/new.html.erb
@@ -0,0 +1,37 @@
+<% content_for(:header) do %>
+  New VAT Rate
+<% end %>
+
+<% content_for(:actions) do %>
+  <%= secondary_action_button do %>
+    <%= link_to 'Back', available_vat_rates_path %>
+  <% end %>
+
+  <%= primary_form_action_button do %>
+    Create VAT Rate
+  <% end %>
+<% end %>
+
+
+<%= form_tag({ controller: "available_vat_rates", action: "create" }, method: "post", id: "form") do %>
+  <div>
+    <label for="code" class="block font-bold">
+      Code
+    </label>
+    <%= text_field_tag :code, "", required: true, class: "mt-1 focus:ring-blue-500 focus:border-blue-500 block shadow-sm sm:text-sm border-gray-300 rounded-md", data: { turbo_permanent: true } %>
+  </div>
+  <div class="mt-2">
+    <label for="rate" class="block font-bold">
+      Rate
+    </label>
+    <%= number_field_tag :rate, nil, min: 0, step: 1.0, required: true, class: "mt-1 focus:ring-blue-500 focus:border-blue-500 block shadow-sm sm:text-sm border-gray-300 rounded-md", data: { turbo_permanent: true } %>
+  </div>
+
+  <% if defined?(errors) %>
+    <% errors.each do |error| %>
+      <div class="mt-2 text-red-600">
+        <span><%= error.full_message %></span>
+      </div>
+    <% end %>
+  <% end %>
+<% end %>
diff --git a/rails_application/app/views/products/new.html.erb b/rails_application/app/views/products/new.html.erb
index bb2480093..da28481d1 100644
--- a/rails_application/app/views/products/new.html.erb
+++ b/rails_application/app/views/products/new.html.erb
@@ -32,7 +32,7 @@
       <label for="vat_rate" class="block font-bold">
         VAT rate
       </label>
-      <%= select_tag :vat_rate, options_from_collection_for_select(Taxes::Configuration.available_vat_rates, :code, :code), class: "mt-1 focus:ring-blue-500 focus:border-blue-500 block shadow-sm sm:text-sm border-gray-300 rounded-md", data: { turbo_permanent: true } %>
+      <%= select_tag :vat_rate, options_from_collection_for_select(VatRates::AvailableVatRate.all, :code, :code), class: "mt-1 focus:ring-blue-500 focus:border-blue-500 block shadow-sm sm:text-sm border-gray-300 rounded-md", data: { turbo_permanent: true } %>
     </div>
 
     <% if defined?(errors) %>
@@ -43,4 +43,4 @@
       <% end %>
     <% end %>
   <% end %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/rails_application/config/routes.rb b/rails_application/config/routes.rb
index 323df11c3..6ac9fcf9a 100644
--- a/rails_application/config/routes.rb
+++ b/rails_application/config/routes.rb
@@ -44,6 +44,9 @@
       post :remove_item
     end
   end
+
+  resources :available_vat_rates, only: [:new, :create, :index]
+
   post :login, to: "client/clients#login"
   get :logout, to: "client/clients#logout"
   get "clients", to: "client/clients#index"
diff --git a/rails_application/db/migrate/20240812080357_create_available_vat_rates.rb b/rails_application/db/migrate/20240812080357_create_available_vat_rates.rb
new file mode 100644
index 000000000..080759089
--- /dev/null
+++ b/rails_application/db/migrate/20240812080357_create_available_vat_rates.rb
@@ -0,0 +1,11 @@
+class CreateAvailableVatRates < ActiveRecord::Migration[7.0]
+  def change
+    create_table :available_vat_rates do |t|
+      t.uuid :uid, null: false
+      t.string :code, null: false
+      t.decimal :rate, null: false
+
+      t.timestamps
+    end
+  end
+end
diff --git a/rails_application/db/schema.rb b/rails_application/db/schema.rb
index 6a06abb5d..5b991f319 100644
--- a/rails_application/db/schema.rb
+++ b/rails_application/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.0].define(version: 2023_09_06_105318) do
+ActiveRecord::Schema[7.0].define(version: 2024_08_12_080357) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
   enable_extension "plpgsql"
@@ -26,6 +26,14 @@
     t.integer "available"
   end
 
+  create_table "available_vat_rates", force: :cascade do |t|
+    t.uuid "uid", null: false
+    t.string "code", null: false
+    t.decimal "rate", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
   create_table "client_order_lines", force: :cascade do |t|
     t.string "order_uid"
     t.string "product_name"
diff --git a/rails_application/db/seeds.rb b/rails_application/db/seeds.rb
index a60141af3..6b2a88b2a 100644
--- a/rails_application/db/seeds.rb
+++ b/rails_application/db/seeds.rb
@@ -36,6 +36,18 @@
   )
 end
 
+[
+  ["20", 20],
+  ["10", 10]
+].each do |vat_rate|
+  command_bus.call(
+    Taxes::AddAvailableVatRate.new(
+      available_vat_rate_id: SecureRandom.uuid,
+      vat_rate: Infra::Types::VatRate.new(code: vat_rate[0], rate: vat_rate[1])
+    )
+  )
+end
+
 [
   ["Fearless Refactoring: Rails controllers", 49],
   ["Rails meets React.js", 49],
@@ -47,7 +59,7 @@
     ProductCatalog::RegisterProduct.new(product_id: product_id),
     ProductCatalog::NameProduct.new(product_id: product_id, name: name_price_tuple[0]),
     Pricing::SetPrice.new(product_id: product_id, price: name_price_tuple[1]),
-    Taxes::SetVatRate.new(product_id: product_id, vat_rate: Taxes::Configuration.available_vat_rates.first)
+    Taxes::SetVatRate.new(product_id: product_id, vat_rate_code: "20")
   ].each do |command|
     command_bus.call(command)
   end
diff --git a/rails_application/lib/configuration.rb b/rails_application/lib/configuration.rb
index 078f236c2..512be0705 100644
--- a/rails_application/lib/configuration.rb
+++ b/rails_application/lib/configuration.rb
@@ -16,14 +16,11 @@ def call(event_store, command_bus)
     enable_shipments_read_model(event_store)
     enable_availability_read_model(event_store)
     enable_authentication_read_model(event_store)
+    enable_vat_rates_read_model(event_store)
 
     Ecommerce::Configuration.new(
       number_generator: Rails.configuration.number_generator,
-      payment_gateway: Rails.configuration.payment_gateway,
-      available_vat_rates: [
-        Infra::Types::VatRate.new(code: "10", rate: 10),
-        Infra::Types::VatRate.new(code: "20", rate: 20)
-    ]
+      payment_gateway: Rails.configuration.payment_gateway
     ).call(event_store, command_bus)
   end
 
@@ -80,4 +77,8 @@ def enable_availability_read_model(event_store)
   def enable_authentication_read_model(event_store)
     ClientAuthentication::Configuration.new.call(event_store)
   end
+
+  def enable_vat_rates_read_model(event_store)
+    VatRates::Configuration.new.call(event_store)
+  end
 end
diff --git a/rails_application/test/integration/available_vat_rates_test.rb b/rails_application/test/integration/available_vat_rates_test.rb
new file mode 100644
index 000000000..1b08d1e99
--- /dev/null
+++ b/rails_application/test/integration/available_vat_rates_test.rb
@@ -0,0 +1,52 @@
+require "test_helper"
+
+class AvailableVatRatesTest < InMemoryRESIntegrationTestCase
+  def test_happy_path
+    get "/available_vat_rates/new"
+    assert_select "h1", "New VAT Rate"
+
+    post "/available_vat_rates",
+         params: {
+           "authenticity_token" => "[FILTERED]",
+           "code" => "10.0",
+           "rate" => 10.0
+         }
+    follow_redirect!
+
+    assert_equal "VAT rate was successfully created", flash[:notice]
+    assert_select "h1", "VAT Rates"
+  end
+
+  def test_validation_errors
+    post "/available_vat_rates",
+         params: {
+           "authenticity_token" => "[FILTERED]",
+           "code" => "",
+           "rate" => ""
+         }
+    assert_response :unprocessable_entity
+
+    assert_select "h1", "New VAT Rate"
+    assert_select "span", "Code can't be blank"
+    assert_select "span", "Rate can't be blank"
+  end
+
+  def test_vat_rate_already_exists
+    post "/available_vat_rates",
+        params: {
+          "authenticity_token" => "[FILTERED]",
+          "code" => "10.0",
+          "rate" => 10.0
+        }
+
+    post "/available_vat_rates",
+        params: {
+          "authenticity_token" => "[FILTERED]",
+          "code" => "10.0",
+          "rate" => 10.0
+        }
+
+    assert_response :unprocessable_entity
+    assert_select "#notice", "VAT rate already exists"
+  end
+end
diff --git a/rails_application/test/integration/client_orders_test.rb b/rails_application/test/integration/client_orders_test.rb
index ff2d50b1e..66bcc8463 100644
--- a/rails_application/test/integration/client_orders_test.rb
+++ b/rails_application/test/integration/client_orders_test.rb
@@ -9,6 +9,7 @@ def setup
     ClientOrders::Client.destroy_all
     ClientOrders::Order.destroy_all
     Orders::Order.destroy_all
+    add_available_vat_rate(10)
   end
 
   def test_happy_path
diff --git a/rails_application/test/integration/customers_test.rb b/rails_application/test/integration/customers_test.rb
index e6c187010..805b3e9db 100644
--- a/rails_application/test/integration/customers_test.rb
+++ b/rails_application/test/integration/customers_test.rb
@@ -1,6 +1,11 @@
 require "test_helper"
 
 class CustomersTest < InMemoryRESIntegrationTestCase
+  def setup
+    super
+    add_available_vat_rate(10)
+  end
+
   def test_list_customers
     get "/customers"
     assert_response :success
diff --git a/rails_application/test/integration/discount_test.rb b/rails_application/test/integration/discount_test.rb
index 804b855bc..a96cfbe9f 100644
--- a/rails_application/test/integration/discount_test.rb
+++ b/rails_application/test/integration/discount_test.rb
@@ -4,6 +4,7 @@ class DiscountTest < InMemoryRESIntegrationTestCase
   def setup
     super
     Orders::Order.destroy_all
+    add_available_vat_rate(10)
   end
 
   def test_reset_discount
@@ -40,4 +41,3 @@ def apply_discount_10_percent(order_id)
     assert_select("td", "$123.30")
   end
 end
-
diff --git a/rails_application/test/integration/orders_test.rb b/rails_application/test/integration/orders_test.rb
index 4f6bb4a28..5dfc6d768 100644
--- a/rails_application/test/integration/orders_test.rb
+++ b/rails_application/test/integration/orders_test.rb
@@ -5,6 +5,7 @@ def setup
     super
     Rails.configuration.payment_gateway.call.reset
     Orders::Order.destroy_all
+    add_available_vat_rate(10)
   end
 
   def test_submitting_empty_order
diff --git a/rails_application/test/integration/products_test.rb b/rails_application/test/integration/products_test.rb
index e9f8b6943..5df6046ff 100644
--- a/rails_application/test/integration/products_test.rb
+++ b/rails_application/test/integration/products_test.rb
@@ -8,6 +8,7 @@ def setup
   end
 
   def test_happy_path
+    add_available_vat_rate(10)
     register_customer("Arkency")
     product_id = SecureRandom.uuid
 
diff --git a/rails_application/test/integration/public_offer_test.rb b/rails_application/test/integration/public_offer_test.rb
index dd8f0281e..1342ba841 100644
--- a/rails_application/test/integration/public_offer_test.rb
+++ b/rails_application/test/integration/public_offer_test.rb
@@ -3,6 +3,7 @@
 class PublicOfferTest < InMemoryRESIntegrationTestCase
   def setup
     super
+    add_available_vat_rate(10)
   end
 
   def test_happy_path
diff --git a/rails_application/test/invoices/invoices_test.rb b/rails_application/test/invoices/invoices_test.rb
index 70ef0220e..c9359fe1c 100644
--- a/rails_application/test/invoices/invoices_test.rb
+++ b/rails_application/test/invoices/invoices_test.rb
@@ -29,27 +29,28 @@ def test_product_name_change_affects_existing_invoices
       product_id = SecureRandom.uuid
       initial_product_name = "Initial Name"
       updated_product_name = "Updated Name"
-      
+
+      add_available_vat_rate(20)
       product_id = register_product(initial_product_name, 100, 20)
       customer_id = register_customer("Test Customer")
-      
+
       order_id = SecureRandom.uuid
       add_product_to_basket(order_id, product_id)
       submit_order(customer_id, order_id)
-      
+
       update_product_name(product_id, updated_product_name)
-      
+
       assert_invoice_product_name(order_id, initial_product_name)
 
       new_order_id = SecureRandom.uuid
       add_product_to_basket(new_order_id, product_id)
       submit_order(customer_id, new_order_id)
-  
+
       assert_invoice_product_name(new_order_id, updated_product_name)
     end
-    
+
     private
-    
+
     def update_product_name(product_id, new_name)
       patch "/products/#{product_id}",
             params: {
@@ -58,11 +59,11 @@ def update_product_name(product_id, new_name)
               name: new_name,
             }
     end
-    
+
     def assert_invoice_product_name(order_id, expected_name)
       get "/invoices/#{order_id}"
       assert_response :success
       assert_select ".py-2", text: expected_name
     end
   end
-end
\ No newline at end of file
+end
diff --git a/rails_application/test/read_model_handler_test.rb b/rails_application/test/read_model_handler_test.rb
index bf9be0b03..0c6f5bc84 100644
--- a/rails_application/test/read_model_handler_test.rb
+++ b/rails_application/test/read_model_handler_test.rb
@@ -106,7 +106,7 @@ def vat_rate_set
   end
 
   def available_vat_rate
-    Taxes::Configuration.available_vat_rates.first
+    Infra::Types::VatRate.new(code: "10", rate: 10)
   end
 
   def event_store
diff --git a/rails_application/test/test_helper.rb b/rails_application/test/test_helper.rb
index acc47ee7f..c6387f6fb 100644
--- a/rails_application/test/test_helper.rb
+++ b/rails_application/test/test_helper.rb
@@ -71,6 +71,10 @@ def register_product(name, price, vat_rate)
     product_id
   end
 
+  def add_available_vat_rate(rate, code = rate.to_s)
+    post "/available_vat_rates", params: { code: code, rate: rate }
+  end
+
   def supply_product(product_id, quantity)
     post "/products/#{product_id}/supplies", params: { quantity: quantity }
   end
diff --git a/rails_application/test/vat_rates/available_vat_rate_added_test.rb b/rails_application/test/vat_rates/available_vat_rate_added_test.rb
new file mode 100644
index 000000000..d7558c1ee
--- /dev/null
+++ b/rails_application/test/vat_rates/available_vat_rate_added_test.rb
@@ -0,0 +1,39 @@
+require "test_helper"
+
+module VatRates
+  class AvailableVatRateAddedTest < InMemoryTestCase
+    cover "VatRates*"
+
+    def test_adding_available_vat_rate
+      uid = SecureRandom.uuid
+      code = "50"
+      rate = 50
+
+      event_store.publish(available_vat_rate_added_event(uid, code, rate))
+      available_vat_rate = AvailableVatRate.find_by_uid(uid)
+
+      assert_equal(uid, available_vat_rate.uid)
+      assert_equal(code, available_vat_rate.code)
+      assert_equal(rate, available_vat_rate.rate)
+    end
+
+    private
+
+    def event_store
+      Rails.configuration.event_store
+    end
+
+    def available_vat_rate_added_event(uid, code, rate)
+      Taxes::AvailableVatRateAdded.new(
+        data: {
+          available_vat_rate_id: uid,
+          vat_rate:
+            {
+              code: code,
+              rate: rate
+            }
+          }
+        )
+    end
+  end
+end

From 2c98985592945e5a30dc17bc1eddccbadf8ffb71 Mon Sep 17 00:00:00 2001
From: marlena-b <marlena.borowiec01@gmail.com>
Date: Mon, 19 Aug 2024 21:35:30 +0200
Subject: [PATCH 2/2] Fix vat rate numeric validation

---
 .../available_vat_rates_controller.rb         |  2 +-
 .../integration/available_vat_rates_test.rb   | 19 +++++++++++++++----
 2 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/rails_application/app/controllers/available_vat_rates_controller.rb b/rails_application/app/controllers/available_vat_rates_controller.rb
index 34f3debf5..783461bdd 100644
--- a/rails_application/app/controllers/available_vat_rates_controller.rb
+++ b/rails_application/app/controllers/available_vat_rates_controller.rb
@@ -11,7 +11,7 @@ def initialize(params)
     end
 
     validates :code, presence: true
-    validates :rate, presence: true, numericality: { only_numeric: true, greater_than: 0 }
+    validates :rate, presence: true, numericality: { greater_than: 0 }
   end
 
   def new
diff --git a/rails_application/test/integration/available_vat_rates_test.rb b/rails_application/test/integration/available_vat_rates_test.rb
index 1b08d1e99..20ae6fed2 100644
--- a/rails_application/test/integration/available_vat_rates_test.rb
+++ b/rails_application/test/integration/available_vat_rates_test.rb
@@ -9,7 +9,7 @@ def test_happy_path
          params: {
            "authenticity_token" => "[FILTERED]",
            "code" => "10.0",
-           "rate" => 10.0
+           "rate" => "10.0"
          }
     follow_redirect!
 
@@ -17,7 +17,7 @@ def test_happy_path
     assert_select "h1", "VAT Rates"
   end
 
-  def test_validation_errors
+  def test_validation_blank_errors
     post "/available_vat_rates",
          params: {
            "authenticity_token" => "[FILTERED]",
@@ -31,19 +31,30 @@ def test_validation_errors
     assert_select "span", "Rate can't be blank"
   end
 
+  def test_validation_rate_must_be_numeric
+    post "/available_vat_rates",
+         params: {
+           "authenticity_token" => "[FILTERED]",
+           "code" => "test",
+           "rate" => "not a number"
+         }
+    assert_response :unprocessable_entity
+    assert_select "span", "Rate is not a number"
+  end
+
   def test_vat_rate_already_exists
     post "/available_vat_rates",
         params: {
           "authenticity_token" => "[FILTERED]",
           "code" => "10.0",
-          "rate" => 10.0
+          "rate" => "10.0"
         }
 
     post "/available_vat_rates",
         params: {
           "authenticity_token" => "[FILTERED]",
           "code" => "10.0",
-          "rate" => 10.0
+          "rate" => "10.0"
         }
 
     assert_response :unprocessable_entity