diff --git a/admin/app/components/solidus_admin/base_component.rb b/admin/app/components/solidus_admin/base_component.rb index e39c1707a4b..37daa9fea48 100644 --- a/admin/app/components/solidus_admin/base_component.rb +++ b/admin/app/components/solidus_admin/base_component.rb @@ -9,6 +9,7 @@ class BaseComponent < ViewComponent::Base include SolidusAdmin::ComponentsHelper include SolidusAdmin::StimulusHelper include SolidusAdmin::VoidElementsHelper + include SolidusAdmin::SolidusFormHelper include Turbo::FramesHelper def icon_tag(name, **attrs) diff --git a/admin/app/components/solidus_admin/tax_rates/edit/component.html.erb b/admin/app/components/solidus_admin/tax_rates/edit/component.html.erb new file mode 100644 index 00000000000..c2dab459474 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/edit/component.html.erb @@ -0,0 +1,19 @@ + + +<%= page id: :resource_modal do %> + <%= page_header do %> + <%= page_header_back(solidus_admin.tax_rates_path) %> + <%= page_header_title(t(".title")) %> + <%= page_header_actions do %> + <%= render component("ui/button").new( + tag: :a, + text: t(".discard"), + href: solidus_admin.tax_rates_path, + scheme: :secondary + ) %> + <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %> + <% end %> + <% end %> + + <%= render component("tax_rates/form").new(tax_rate: @tax_rate, form_url:, form_id:) %> +<% end %> diff --git a/admin/app/components/solidus_admin/tax_rates/edit/component.rb b/admin/app/components/solidus_admin/tax_rates/edit/component.rb new file mode 100644 index 00000000000..583e927f6fd --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/edit/component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SolidusAdmin::TaxRates::Edit::Component < SolidusAdmin::Resources::Edit::Component + include SolidusAdmin::Layout::PageHelpers +end diff --git a/admin/app/components/solidus_admin/tax_rates/edit/component.yml b/admin/app/components/solidus_admin/tax_rates/edit/component.yml new file mode 100644 index 00000000000..e733a6052d8 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/edit/component.yml @@ -0,0 +1,4 @@ +en: + discard: "Discard" + save: "Save" + title: "Edit Tax Rate" diff --git a/admin/app/components/solidus_admin/tax_rates/form/component.html.erb b/admin/app/components/solidus_admin/tax_rates/form/component.html.erb new file mode 100644 index 00000000000..4138434efdf --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/form/component.html.erb @@ -0,0 +1,25 @@ +<%= solidus_form_for @tax_rate, url: @form_url, html: { id: @form_id } do |f| %> + <%= page_with_sidebar do %> + <%= page_with_sidebar_main do %> + <%= render component("ui/panel").new do %> + <%= f.text_field(:name) %> + <%= f.select(:zone_id, zone_options) %> + <%= f.select(:tax_category_ids, tax_category_options, multiple: true) %> + <%= f.switch_field(:show_rate_in_label, hint: t(".hints.show_rate_in_label")) %> + <% end %> + + <%= render component("ui/panel").new(title: t(".calculation")) do %> + <%= f.select(:calculator_type, calculator_options) %> + <%= f.text_field(:amount, type: :number, step: 0.01) %> + <%= f.select(:level, level_options) %> + <%= f.switch_field(:included_in_price, hint: t(".hints.included_in_price")) %> + <% end %> + <% end %> + <%= page_with_sidebar_aside do %> + <%= render component("ui/panel").new(title: t(".validity")) do %> + <%= f.text_field(:starts_at, type: :date) %> + <%= f.text_field(:expires_at, type: :date) %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/tax_rates/form/component.rb b/admin/app/components/solidus_admin/tax_rates/form/component.rb new file mode 100644 index 00000000000..bf22564cf88 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/form/component.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class SolidusAdmin::TaxRates::Form::Component < SolidusAdmin::BaseComponent + include SolidusAdmin::Layout::PageHelpers + + def initialize(tax_rate:, form_url:, form_id:) + @tax_rate = tax_rate + @form_url = form_url + @form_id = form_id + end + + private + + def zone_options + @zone_options ||= Spree::Zone.order(:name).map { [_1.name, _1.id] } + end + + def tax_category_options + @tax_category_options ||= Spree::TaxCategory.order(:name).map { [_1.name, _1.id] } + end + + def calculator_options + @calculator_options ||= Rails.application.config.spree.calculators.tax_rates.map { [_1.description, _1.name] } + end + + def level_options + @level_options ||= Spree::TaxRate.levels.keys.map { [t(".levels.#{_1}"), _1] } + end +end diff --git a/admin/app/components/solidus_admin/tax_rates/form/component.yml b/admin/app/components/solidus_admin/tax_rates/form/component.yml new file mode 100644 index 00000000000..81cc1df652c --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/form/component.yml @@ -0,0 +1,9 @@ +en: + calculation: "Calculation" + hints: + included_in_price: "Enables automatic inclusion of the applicable tax amount within product prices, providing customers with an all-inclusive pricing experience." + show_rate_in_label: "Allows users to display the applicable tax rate alongside product prices for transparent and informative pricing." + levels: + item: "Item level" + order: "Order level" + validity: "Validity" diff --git a/admin/app/components/solidus_admin/tax_rates/index/component.rb b/admin/app/components/solidus_admin/tax_rates/index/component.rb index 148d280a682..bad513f4982 100644 --- a/admin/app/components/solidus_admin/tax_rates/index/component.rb +++ b/admin/app/components/solidus_admin/tax_rates/index/component.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class SolidusAdmin::TaxRates::Index::Component < SolidusAdmin::Taxes::Component - def row_url(tax_rate) - spree.edit_admin_tax_rate_path(tax_rate) + def edit_path(tax_rate) + solidus_admin.edit_tax_rate_path(tax_rate) end def model_class @@ -13,11 +13,11 @@ def search_url solidus_admin.tax_rates_path end - def actions + def page_actions render component("ui/button").new( tag: :a, text: t('.add'), - href: spree.new_admin_tax_rate_path, + href: solidus_admin.new_tax_rate_path, icon: "add-line", class: "align-self-end w-full", ) @@ -59,16 +59,27 @@ def columns [ { header: :zone, - data: -> { _1.zone&.name }, + data: ->(tax_rate) do + link_to tax_rate.zone.name, edit_path(tax_rate), class: 'body-link' if tax_rate.zone.present? + end + }, + { + header: :name, + data: ->(tax_rate) do + link_to tax_rate.name, edit_path(tax_rate), class: 'body-link' + end }, - :name, { header: :tax_categories, - data: -> { _1.tax_categories.map(&:name).join(', ') }, + data: ->(tax_rate) do + link_to tax_rate.tax_categories.map(&:name).join(', '), edit_path(tax_rate), class: 'body-link' + end }, { header: :amount, - data: -> { _1.display_amount }, + data: ->(tax_rate) do + link_to tax_rate.display_amount, edit_path(tax_rate), class: 'body-link' + end }, { header: :included_in_price, @@ -78,10 +89,17 @@ def columns header: :show_rate_in_label, data: -> { _1.show_rate_in_label? ? component('ui/badge').yes : component('ui/badge').no }, }, - :expires_at, + { + header: :expires_at, + data: ->(tax_rate) do + link_to tax_rate.expires_at.to_date, edit_path(tax_rate), class: 'body-link' if tax_rate.expires_at + end + }, { header: Spree::Calculator.model_name.human, - data: -> { _1.calculator&.class&.model_name&.human } + data: ->(tax_rate) do + link_to tax_rate.calculator&.class&.model_name&.human, edit_path(tax_rate), class: 'body-link' + end }, ] end diff --git a/admin/app/components/solidus_admin/tax_rates/new/component.html.erb b/admin/app/components/solidus_admin/tax_rates/new/component.html.erb new file mode 100644 index 00000000000..c2dab459474 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/new/component.html.erb @@ -0,0 +1,19 @@ + + +<%= page id: :resource_modal do %> + <%= page_header do %> + <%= page_header_back(solidus_admin.tax_rates_path) %> + <%= page_header_title(t(".title")) %> + <%= page_header_actions do %> + <%= render component("ui/button").new( + tag: :a, + text: t(".discard"), + href: solidus_admin.tax_rates_path, + scheme: :secondary + ) %> + <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %> + <% end %> + <% end %> + + <%= render component("tax_rates/form").new(tax_rate: @tax_rate, form_url:, form_id:) %> +<% end %> diff --git a/admin/app/components/solidus_admin/tax_rates/new/component.rb b/admin/app/components/solidus_admin/tax_rates/new/component.rb new file mode 100644 index 00000000000..f936c3e0fb7 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/new/component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SolidusAdmin::TaxRates::New::Component < SolidusAdmin::Resources::New::Component + include SolidusAdmin::Layout::PageHelpers +end diff --git a/admin/app/components/solidus_admin/tax_rates/new/component.yml b/admin/app/components/solidus_admin/tax_rates/new/component.yml new file mode 100644 index 00000000000..e29bdc5a8d2 --- /dev/null +++ b/admin/app/components/solidus_admin/tax_rates/new/component.yml @@ -0,0 +1,4 @@ +en: + discard: "Discard" + save: "Save" + title: "New Tax Rate" diff --git a/admin/app/controllers/solidus_admin/tax_rates_controller.rb b/admin/app/controllers/solidus_admin/tax_rates_controller.rb index 75c2f239eb4..47821975890 100644 --- a/admin/app/controllers/solidus_admin/tax_rates_controller.rb +++ b/admin/app/controllers/solidus_admin/tax_rates_controller.rb @@ -1,35 +1,20 @@ # frozen_string_literal: true module SolidusAdmin - class TaxRatesController < SolidusAdmin::BaseController - include SolidusAdmin::ControllerHelpers::Search - - def index - tax_rates = apply_search_to( - Spree::TaxRate.order(created_at: :desc, id: :desc), - param: :q, - ) - - set_page_and_extract_portion_from(tax_rates) - - respond_to do |format| - format.html { render component('tax_rates/index').new(page: @page) } - end - end - - def destroy - @tax_rates = Spree::TaxRate.where(id: params[:id]) + class TaxRatesController < SolidusAdmin::ResourcesController + private - Spree::TaxRate.transaction { @tax_rates.destroy_all } + def resource_class = Spree::TaxRate - flash[:notice] = t('.success') - redirect_back_or_to tax_rates_path, status: :see_other + def resources_collection + resource_class.includes(:zone, :tax_categories, :calculator) end - private + def resources_sorting_options = { created_at: :desc, id: :desc } - def tax_rate_params - params.require(:tax_rate).permit(:tax_rate_id, permitted_tax_rate_attributes) + def permitted_resource_params + params.require(:tax_rate).permit(:name, :zone_id, :show_rate_in_label, :calculator_type, :amount, :level, + :included_in_price, :starts_at, :expires_at, tax_category_ids: []) end end end diff --git a/admin/app/helpers/solidus_admin/solidus_form_helper.rb b/admin/app/helpers/solidus_admin/solidus_form_helper.rb new file mode 100644 index 00000000000..c4a335ba930 --- /dev/null +++ b/admin/app/helpers/solidus_admin/solidus_form_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module SolidusAdmin + module SolidusFormHelper + def solidus_form_for(*args, **kwargs, &block) + form_for(*args, **kwargs, builder: SolidusAdmin::FormBuilder, &block) + end + end +end diff --git a/admin/app/lib/solidus_admin/form_builder.rb b/admin/app/lib/solidus_admin/form_builder.rb new file mode 100644 index 00000000000..2293f8c5748 --- /dev/null +++ b/admin/app/lib/solidus_admin/form_builder.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class SolidusAdmin::FormBuilder < ActionView::Helpers::FormBuilder + include SolidusAdmin::ComponentsHelper + + delegate :render, to: :@template + + def text_field(method, **options) + render component("ui/forms/field").text_field(self, method, **options) + end + + def text_area(method, **options) + render component("ui/forms/field").text_area(self, method, **options) + end + + def select(method, choices, **options) + render component("ui/forms/field").select(self, method, choices, **options) + end + + def checkbox(method, checked: nil, **options, &block) + checked = checked.nil? ? @object.public_send(method) : checked + component_instance = component("ui/forms/checkbox").new(object_name: @object_name, checked:, method:, **options) + render component_instance, &block + end + + def checkbox_row(method, options:, row_title:, **attrs) + render component("ui/checkbox_row").new(form: self, method:, options:, row_title:, **attrs) + end + + def input(method, **options) + name = "#{@object_name}[#{method}]" + value = @object.public_send(method) if options[:value].nil? + render component("ui/forms/input").new(name:, value:, **options) + end + + def hidden_field(method, **options) + input(method, type: :hidden, autocomplete: "off", **options) + end + + def switch_field(method, label: nil, include_hidden: true, **options) + label = @object.class.human_attribute_name(method) if label.nil? + name = "#{@object_name}[#{method}]" + error = @object.errors[method] + checked = @object.public_send(method) + render component("ui/forms/switch_field").new(label:, name:, error:, checked:, include_hidden:, **options) + end + + def submit(text, **options) + render component("ui/button").new(type: :submit, text:, form: id, **options) + end +end diff --git a/admin/config/locales/tax_rates.en.yml b/admin/config/locales/tax_rates.en.yml index b95ee13ed99..668bd036fd6 100644 --- a/admin/config/locales/tax_rates.en.yml +++ b/admin/config/locales/tax_rates.en.yml @@ -2,5 +2,9 @@ en: solidus_admin: tax_rates: title: "Tax Rates" + create: + success: "Tax rate was successfully created." destroy: success: "Tax rates were successfully removed." + update: + success: "Tax rate was successfully updated." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index ec215522965..125b1544f5e 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -73,7 +73,7 @@ admin_resources :taxonomies, only: [:index, :destroy], sortable: true admin_resources :promotion_categories, only: [:index, :destroy] admin_resources :tax_categories, except: [:show] - admin_resources :tax_rates, only: [:index, :destroy] + admin_resources :tax_rates, except: [:show] admin_resources :payment_methods, only: [:index, :destroy], sortable: true admin_resources :stock_items, only: [:index, :edit, :update] admin_resources :shipping_methods, only: [:index, :destroy] diff --git a/admin/lib/solidus_admin/testing_support/feature_helpers.rb b/admin/lib/solidus_admin/testing_support/feature_helpers.rb index 6f28ad553cb..cd81a9f46c0 100644 --- a/admin/lib/solidus_admin/testing_support/feature_helpers.rb +++ b/admin/lib/solidus_admin/testing_support/feature_helpers.rb @@ -61,6 +61,11 @@ def clear_search find('button[aria-label="Clear"]').click end end + + def switch(locator, on: true) + checkbox = find(:label, text: locator).find(:checkbox) + on ? checkbox.check : checkbox.uncheck + end end end end diff --git a/admin/spec/features/tax_rates_spec.rb b/admin/spec/features/tax_rates_spec.rb index c0e1809c7a9..a5cb4b8cba6 100644 --- a/admin/spec/features/tax_rates_spec.rb +++ b/admin/spec/features/tax_rates_spec.rb @@ -21,4 +21,97 @@ expect(Spree::TaxRate.count).to eq(1) expect(page).to be_axe_clean end + + context "creating new tax rate" do + before do + create(:zone, name: "EU") + create(:tax_category, name: "Default") + create(:tax_category, name: "Specific") + end + + it "creates new tax rate" do + visit "/admin/tax_rates" + click_on "Add new" + expect(page).to have_current_path("/admin/tax_rates/new") + expect(page).to be_axe_clean + + fill_in "Name", with: "Clothing" + solidus_select "EU", from: "Zone" + solidus_select %w[Default Specific], from: "Tax Categories" + switch "Show Rate in Label" + solidus_select "Default Tax", from: "Calculator" + fill_in "Rate", with: "0.18" + solidus_select "Item level", from: "Tax Rate Level" + switch "Included in Price" + fill_in "Start Date", with: 1.month.ago + fill_in "Expiration Date", with: 1.year.from_now + within("header") { click_on "Save" } + + expect(page).to have_current_path("/admin/tax_rates") + expect(page).to have_content("Tax rate was successfully created.") + expect(page).to have_content("EU") + expect(page).to have_content("Clothing") + expect(page).to have_content("Default, Specific") + expect(page).to have_content("18.0%") + expect(page).to have_content(1.year.from_now.to_date.to_s) + expect(page).to have_content("Default Tax") + end + + context "with invalid attributes" do + it "shows validation errors" do + visit "/admin/tax_rates" + click_on "Add new" + + within("header") { click_on "Save" } + + expect(page).to have_current_path("/admin/tax_rates/new") + expect(page).to have_content("can't be blank") + expect(page).to have_content("is not a number") + end + end + end + + context "updating tax rate" do + before do + create(:tax_rate, + name: "Clothing", + zone: create(:zone, name: "US"), + tax_categories: [create(:tax_category, name: "Default")], + amount: 0.3) + create(:zone, name: "EU") + create(:tax_category, name: "Specific") + end + + it "updates tax rate" do + visit "/admin/tax_rates" + click_on "Clothing" + + fill_in "Name", with: "Food" + solidus_select "EU", from: "Zone" + solidus_select "Specific", from: "Tax Categories" + fill_in "Rate", with: "0.18" + + within("header") { click_on "Save" } + + expect(page).to have_content("Tax rate was successfully updated.") + expect(page).to have_content("EU") + expect(page).to have_content("Food") + expect(page).to have_content("Default, Specific") + expect(page).to have_content("18.0%") + end + + context "with invalid attributes" do + it "shows validation errors" do + visit "/admin/tax_rates" + click_on "Clothing" + + fill_in "Rate", with: "" + + within("header") { click_on "Save" } + + expect(page).to have_content("can't be blank") + expect(page).to have_content("is not a number") + end + end + end end diff --git a/admin/spec/requests/solidus_admin/tax_rates_spec.rb b/admin/spec/requests/solidus_admin/tax_rates_spec.rb new file mode 100644 index 00000000000..9bc7288160b --- /dev/null +++ b/admin/spec/requests/solidus_admin/tax_rates_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "spec_helper" +require 'solidus_admin/testing_support/shared_examples/crud_resource_requests' + +RSpec.describe "SolidusAdmin::TaxRatesController", type: :request do + include_examples 'CRUD resource requests', 'tax_rate' do + let(:resource_class) { Spree::TaxRate } + let(:valid_attributes) { { amount: 1, calculator_type: "Spree::Calculator::DefaultTax" } } + let(:invalid_attributes) { { amount: "", calculator_type: nil } } + end +end diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index ad8164abb08..29ca4c06853 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -394,12 +394,15 @@ en: tax_code: Tax Code spree/tax_rate: amount: Rate + calculator_type: Calculator expires_at: Expiration Date included_in_price: Included in Price + level: Tax Rate Level name: Name show_rate_in_label: Show Rate in Label starts_at: Start Date tax_categories: Tax Categories + tax_category_ids: Tax Categories spree/taxon: description: Description icon: Icon