From a1b1502fd08d6bde2c04e74c72a680bb5434494b Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 3 Mar 2025 23:02:58 +0530 Subject: [PATCH] [Compliance / SEO] Add structured data fields to Spree stores This Commit provides the possibility to store legal information in the store resource. Given the wide array of different information required by country, the information stored here provides a baseline for most countries, but might need to be augmented in some jurisdictions. This commit is not meant as legal advisory. Inform yourself about eventual requirements in your country of operation. Some additional information that might be needed: - Legal Representative, - Share Capital of the company, - Contacts for privacy. Schema.org has a schema to markup organizations that has been widely adopted by various search engines: | | https://schema.org/Organization Support / Docs | |--------|-----------------------------------------------------------------------------------| | Bing | https://www.bing.com/webmasters/help/?topicid=cc507c09 | | Google | https://developers.google.com/search/docs/appearance/structured-data/organization | | Yandex | https://yandex.com/support/webmaster/schema-org/what-is-schema-org.html | This update allows to return all organization data on frontend via jsonb if implemented according to schema documentation on a per store basis and edit the data via API and Backend (Old / New). The implemented fields are: - legal_name, - contact_email, - contact_phone, - vat_id, - tax_id, - address1, - address2, - Zip code and city, - state_name, - country_id, - and state_id. Some additional notes regarding the size of the commit: While this commit seems massive in size, no functionality has been added apart from storing the values, no logical changes have been made, hence the commit while bigger in size is simple to analyse. Apart from testing routines, this commit does not contain functional code. Where possible the structure of current address forms has been adopted. Fixes #6173 --- .../stores/address_form/component.html.erb | 49 ++++ .../stores/address_form/component.js | 56 +++++ .../stores/address_form/component.rb | 15 ++ .../stores/address_form/component.yml | 3 + .../stores/edit/component.html.erb | 87 ++++++++ .../solidus_admin/stores/edit/component.rb | 62 +++++ .../solidus_admin/stores/edit/component.yml | 17 ++ .../stores/new/component.html.erb | 87 ++++++++ .../solidus_admin/stores/new/component.rb | 62 +++++ .../solidus_admin/stores/new/component.yml | 17 ++ .../solidus_admin/stores_controller.rb | 20 ++ admin/config/routes.rb | 2 +- .../stores/address_form/component_spec.rb | 35 +++ .../stores/edit/component_spec.rb | 61 +++++ .../stores/new/component_spec.rb | 57 +++++ .../requests/solidus_admin/stores_spec.rb | 40 ++++ api/lib/spree/api_configuration.rb | 7 +- api/openapi/solidus-api.oas.yml | 54 +++++ api/spec/requests/spree/api/stores_spec.rb | 134 ++++++++++- .../spree/admin/stores/_address_form.html.erb | 75 +++++++ .../views/spree/admin/stores/_form.html.erb | 211 +++++++++++------- core/app/models/spree/store.rb | 4 + core/config/locales/en.yml | 23 +- ...07_add_store_attributes_to_spree_stores.rb | 19 ++ core/lib/spree/permitted_attributes.rb | 6 +- 25 files changed, 1116 insertions(+), 87 deletions(-) create mode 100644 admin/app/components/solidus_admin/stores/address_form/component.html.erb create mode 100644 admin/app/components/solidus_admin/stores/address_form/component.js create mode 100644 admin/app/components/solidus_admin/stores/address_form/component.rb create mode 100644 admin/app/components/solidus_admin/stores/address_form/component.yml create mode 100644 admin/app/components/solidus_admin/stores/edit/component.html.erb create mode 100644 admin/app/components/solidus_admin/stores/edit/component.rb create mode 100644 admin/app/components/solidus_admin/stores/edit/component.yml create mode 100644 admin/app/components/solidus_admin/stores/new/component.html.erb create mode 100644 admin/app/components/solidus_admin/stores/new/component.rb create mode 100644 admin/app/components/solidus_admin/stores/new/component.yml create mode 100644 admin/spec/components/solidus_admin/stores/address_form/component_spec.rb create mode 100644 admin/spec/components/solidus_admin/stores/edit/component_spec.rb create mode 100644 admin/spec/components/solidus_admin/stores/new/component_spec.rb create mode 100644 admin/spec/requests/solidus_admin/stores_spec.rb create mode 100644 backend/app/views/spree/admin/stores/_address_form.html.erb create mode 100644 core/db/migrate/20250202173007_add_store_attributes_to_spree_stores.rb diff --git a/admin/app/components/solidus_admin/stores/address_form/component.html.erb b/admin/app/components/solidus_admin/stores/address_form/component.html.erb new file mode 100644 index 00000000000..8faf04141ab --- /dev/null +++ b/admin/app/components/solidus_admin/stores/address_form/component.html.erb @@ -0,0 +1,49 @@ +
+
+ <%= render component("ui/forms/field").text_field(@name, :legal_name, object: @store) %> + <%= render component("ui/forms/field").text_field(@name, :address1, object: @store) %> + <%= render component("ui/forms/field").text_field(@name, :address2, object: @store) %> +
+ <%= render component("ui/forms/field").text_field(@name, :city, object: @store) %> + <%= render component("ui/forms/field").text_field(@name, :zipcode, object: @store) %> +
+ + <%= render component("ui/forms/field").select( + @name, + :country_id, + Spree::Country.pluck(:name, :id), + object: @store, + value: @store.country_id, + "data-#{stimulus_id}-target": "country", + "data-action": "change->#{stimulus_id}#loadStates" + ) %> + <%= content_tag :div, + class: "flex flex-col gap-2 w-full #{'hidden' if @store.country&.states_required}", + data: { "#{stimulus_id}-target": "stateNameWrapper" } do %> + <%= render component("ui/forms/field").text_field( + @name, :state_name, + object: @store, + value: @store.state_name, + data: { "#{stimulus_id}-target": "stateName" } + ) %> + <% end %> + + > + + <%= content_tag :div, + class: "flex flex-col gap-2 w-full #{'hidden' unless @store.country&.states_required}", + data: { "#{stimulus_id}-target": "stateWrapper" } do %> + <%= render component("ui/forms/field").select( + @name, :state_id, + state_options, + object: @store, + value: @store.state_id, + data: { "#{stimulus_id}-target": "state" } + ) %> + <% end %> + <%= render component("ui/forms/field").text_field(@name, :tax_id, object: @store) %> + <%= render component("ui/forms/field").text_field(@name, :vat_id, object: @store, hint: t(".hint.vat_id").html_safe) %> +
+
diff --git a/admin/app/components/solidus_admin/stores/address_form/component.js b/admin/app/components/solidus_admin/stores/address_form/component.js new file mode 100644 index 00000000000..1d57e7eb40b --- /dev/null +++ b/admin/app/components/solidus_admin/stores/address_form/component.js @@ -0,0 +1,56 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["country", "state", "stateName", "stateWrapper", "stateNameWrapper"] + + loadStates() { + const countryId = this.countryTarget.value + + fetch(`/admin/countries/${countryId}/states`) + .then((response) => response.json()) + .then((data) => { + this.updateStateOptions(data) + }) + } + + updateStateOptions(states) { + if (states.length === 0) { + this.toggleStateFields(false) + } else { + this.toggleStateFields(true) + this.populateStateSelect(states) + } + } + + toggleStateFields(showSelect) { + const stateWrapper = this.stateWrapperTarget + const stateNameWrapper = this.stateNameWrapperTarget + const stateSelect = this.stateTarget + const stateName = this.stateNameTarget + + if (showSelect) { + // Show state select dropdown. + stateSelect.disabled = false + stateName.value = "" + stateWrapper.classList.remove("hidden") + stateNameWrapper.classList.add("hidden") + } else { + // Show state name text input if no states to choose from. + stateSelect.disabled = true + stateWrapper.classList.add("hidden") + stateNameWrapper.classList.remove("hidden") + } + } + + populateStateSelect(states) { + const stateSelect = this.stateTarget + stateSelect.innerHTML = "" + + states.forEach((state) => { + const option = document.createElement("option") + option.value = state.id + option.innerText = state.name + stateSelect.appendChild(option) + }) + } +} diff --git a/admin/app/components/solidus_admin/stores/address_form/component.rb b/admin/app/components/solidus_admin/stores/address_form/component.rb new file mode 100644 index 00000000000..cd3e96162d1 --- /dev/null +++ b/admin/app/components/solidus_admin/stores/address_form/component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class SolidusAdmin::Stores::AddressForm::Component < SolidusAdmin::BaseComponent + def initialize(store:) + @name = "store" + @store = store + end + + def state_options + country = @store.country + return [] unless country && country.states_required + + country.states.pluck(:name, :id) + end +end diff --git a/admin/app/components/solidus_admin/stores/address_form/component.yml b/admin/app/components/solidus_admin/stores/address_form/component.yml new file mode 100644 index 00000000000..ab667a9d0c5 --- /dev/null +++ b/admin/app/components/solidus_admin/stores/address_form/component.yml @@ -0,0 +1,3 @@ +en: + hint: + vat_id: Enter your VAT-ID without the Country Prefix (eg IT for Italy or DE for Germany) but solely the identification string. diff --git a/admin/app/components/solidus_admin/stores/edit/component.html.erb b/admin/app/components/solidus_admin/stores/edit/component.html.erb new file mode 100644 index 00000000000..b7658bfe38e --- /dev/null +++ b/admin/app/components/solidus_admin/stores/edit/component.html.erb @@ -0,0 +1,87 @@ +<%= page do %> + <%= page_header do %> + <%= page_header_back(solidus_admin.stores_path) %> + <%= page_header_title(t(".title", store: @store&.name)) %> + <%= page_header_actions do %> +
+ <%= render component("ui/button").new(tag: :button, text: t(".update"), form: form_id) %> + <%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.edit_store_path(@store), scheme: :secondary) %> +
+ <% end %> + <% end %> + + <%= form_for @store, url: solidus_admin.store_path(@store), html: { id: form_id } do |f| %> + <%= page_with_sidebar do %> + <%= page_with_sidebar_main do %> + <%= render component("ui/panel").new(title: t(".store_settings")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :name, required: true) %> + <%= render component("ui/forms/field").text_field(f, :url, required: true) %> + <%= render component("ui/forms/field").text_field(f, :code, required: true, hint: t(".hint.code").html_safe) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".regional_settings")) do %> +
+ <%= render component("ui/forms/field").select( + f, + :default_currency, + currency_options, + include_blank: true, + hint: t(".hint.default_currency").html_safe + ) %> + <%= render component("ui/forms/field").select( + f, + :cart_tax_country_iso, + cart_tax_country_options, + include_blank: t(".no_cart_tax_country"), + hint: t(".hint.cart_tax_country_iso").html_safe + ) %> + <%= render component("ui/forms/field").select( + f, + :available_locales, + localization_options, + multiple: true, + class: "select2", + name: "store[available_locales][]", + hint: t(".hint.available_locales").html_safe + ) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".email_settings")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %> + <%= render component("ui/forms/field").text_field(f, :bcc_email) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".store_legal_addres")) do %> +
+
+ <%= render component("stores/address_form").new( + store: @store, + ) %> +
+
+ <% end %> + + <%= render component("ui/panel").new(title: t(".contact_options")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :contact_phone) %> + <%= render component("ui/forms/field").text_field(f, :contact_email) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".content_on_storefront")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :seo_title) %> + <%= render component("ui/forms/field").text_area(f, :description) %> + <%= render component("ui/forms/field").text_field(f, :meta_keywords) %> + <%= render component("ui/forms/field").text_area(f, :meta_description) %> +
+ <% end %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/stores/edit/component.rb b/admin/app/components/solidus_admin/stores/edit/component.rb new file mode 100644 index 00000000000..8c170c0a2c9 --- /dev/null +++ b/admin/app/components/solidus_admin/stores/edit/component.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class SolidusAdmin::Stores::Edit::Component < SolidusAdmin::BaseComponent + include SolidusAdmin::Layout::PageHelpers + + # Define the necessary attributes for the component + attr_reader :store, :available_countries + + # Initialize the component with required data + def initialize(store:) + @store = store + @available_countries = fetch_available_countries + end + + def form_id + @form_id ||= "#{stimulus_id}--form-#{@store.id}" + end + + def currency_options + Spree::Config.available_currencies.map(&:iso_code) + end + + # Generates options for cart tax countries + def cart_tax_country_options + fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]).map do |country| + [country.name, country.iso] + end + end + + # Generates available locales + def localization_options + Spree.i18n_available_locales.map do |locale| + [ + I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s), + locale + ] + end + end + + # Fetch countries for the address form + def available_country_options + Spree::Country.order(:name).map { |country| [country.name, country.id] } + end + + private + + # Fetch the available countries for the localization section + def fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]) + countries = Spree::Country.available(restrict_to_zone:) + + country_names = Carmen::Country.all.map do |country| + [country.code, country.name] + end.to_h + + country_names.update I18n.t('spree.country_names', default: {}).stringify_keys + + countries.collect do |country| + country.name = country_names.fetch(country.iso, country.name) + country + end.sort_by { |country| country.name.parameterize } + end +end diff --git a/admin/app/components/solidus_admin/stores/edit/component.yml b/admin/app/components/solidus_admin/stores/edit/component.yml new file mode 100644 index 00000000000..1c0e1e5b9d4 --- /dev/null +++ b/admin/app/components/solidus_admin/stores/edit/component.yml @@ -0,0 +1,17 @@ +en: + address: Address + cancel: Cancel + contact_options: Contact Options + content_on_storefront: Content on Storefront + email_settings: Email Settings + hint: + available_locales: This determines which locales are available for your customers to choose from in the storefront. + cart_tax_country_iso: 'This determines which country is used for taxes on carts (orders which don''t yet have an address).
Default: None.' + code: An identifier for your store. Developers may need this value if you operate multiple storefronts. + default_currency: This determines which currency will be used for the storefront's product prices. Please, be aware that changing this configuration, only products that have prices in the selected currency will be listed on your storefront.
This setting won't change the default currency used when you create a product. For that, only the global `Spree::Config.currency` is taken into account. + regional_settings: Regional Settings + store_details: Store Details + store_legal_addres: Store Legal Address + store_settings: Store Name & URL / URI Settings + title: "%{store}" + update: Update diff --git a/admin/app/components/solidus_admin/stores/new/component.html.erb b/admin/app/components/solidus_admin/stores/new/component.html.erb new file mode 100644 index 00000000000..709730bb2ba --- /dev/null +++ b/admin/app/components/solidus_admin/stores/new/component.html.erb @@ -0,0 +1,87 @@ +<%= page do %> + <%= page_header do %> + <%= page_header_back(solidus_admin.stores_path) %> + <%= page_header_title(t(".title")) %> + <%= page_header_actions do %> +
+ <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %> + <%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.new_store_path, scheme: :secondary) %> +
+ <% end %> + <% end %> + + <%= form_for @store, url: solidus_admin.stores_path, html: { id: form_id } do |f| %> + <%= page_with_sidebar do %> + <%= page_with_sidebar_main do %> + <%= render component("ui/panel").new(title: t(".store_settings")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :name, required: true) %> + <%= render component("ui/forms/field").text_field(f, :url, required: true) %> + <%= render component("ui/forms/field").text_field(f, :code, required: true, hint: t(".hint.code").html_safe) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".regional_settings")) do %> +
+ <%= render component("ui/forms/field").select( + f, + :default_currency, + currency_options, + include_blank: true, + hint: t(".hint.default_currency").html_safe + ) %> + <%= render component("ui/forms/field").select( + f, + :cart_tax_country_iso, + cart_tax_country_options, + include_blank: t(".no_cart_tax_country"), + hint: t(".hint.cart_tax_country_iso").html_safe + ) %> + <%= render component("ui/forms/field").select( + f, + :available_locales, + localization_options, + multiple: true, + class: "select2", + name: "store[available_locales][]", + hint: t(".hint.available_locales").html_safe + ) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".email_settings")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %> + <%= render component("ui/forms/field").text_field(f, :bcc_email) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".store_legal_addres")) do %> +
+
+ <%= render component("stores/address_form").new( + store: @store, + ) %> +
+
+ <% end %> + + <%= render component("ui/panel").new(title: t(".contact_options")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :contact_phone) %> + <%= render component("ui/forms/field").text_field(f, :contact_email) %> +
+ <% end %> + + <%= render component("ui/panel").new(title: t(".content_on_storefront")) do %> +
+ <%= render component("ui/forms/field").text_field(f, :seo_title) %> + <%= render component("ui/forms/field").text_area(f, :description) %> + <%= render component("ui/forms/field").text_field(f, :meta_keywords) %> + <%= render component("ui/forms/field").text_area(f, :meta_description) %> +
+ <% end %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/stores/new/component.rb b/admin/app/components/solidus_admin/stores/new/component.rb new file mode 100644 index 00000000000..a1c7d288f8f --- /dev/null +++ b/admin/app/components/solidus_admin/stores/new/component.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class SolidusAdmin::Stores::New::Component < SolidusAdmin::BaseComponent + include SolidusAdmin::Layout::PageHelpers + + # Define the necessary attributes for the component + attr_reader :store, :available_countries + + # Initialize the component with required data + def initialize(store:) + @store = store + @available_countries = fetch_available_countries + end + + def form_id + @form_id ||= "#{stimulus_id}--form-#{@store.id}" + end + + def currency_options + Spree::Config.available_currencies.map(&:iso_code) + end + + # Generates options for cart tax countries + def cart_tax_country_options + fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]).map do |country| + [country.name, country.iso] + end + end + + # Generates available locales + def localization_options + Spree.i18n_available_locales.map do |locale| + [ + I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s), + locale + ] + end + end + + # Fetch countries for the address form + def available_country_options + Spree::Country.order(:name).map { |country| [country.name, country.id] } + end + + private + + # Fetch the available countries for the localization section + def fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]) + countries = Spree::Country.available(restrict_to_zone:) + + country_names = Carmen::Country.all.map do |country| + [country.code, country.name] + end.to_h + + country_names.update I18n.t('spree.country_names', default: {}).stringify_keys + + countries.collect do |country| + country.name = country_names.fetch(country.iso, country.name) + country + end.sort_by { |country| country.name.parameterize } + end +end diff --git a/admin/app/components/solidus_admin/stores/new/component.yml b/admin/app/components/solidus_admin/stores/new/component.yml new file mode 100644 index 00000000000..ec4272fff11 --- /dev/null +++ b/admin/app/components/solidus_admin/stores/new/component.yml @@ -0,0 +1,17 @@ +en: + address: Address + cancel: Cancel + contact_options: Contact Options + content_on_storefront: Content on Storefront + email_settings: Email Settings + hint: + available_locales: This determines which locales are available for your customers to choose from in the storefront. + cart_tax_country_iso: 'This determines which country is used for taxes on carts (orders which don''t yet have an address).
Default: None.' + code: An identifier for your store. Developers may need this value if you operate multiple storefronts. + default_currency: This determines which currency will be used for the storefront's product prices. Please, be aware that changing this configuration, only products that have prices in the selected currency will be listed on your storefront.
This setting won't change the default currency used when you create a product. For that, only the global `Spree::Config.currency` is taken into account. + regional_settings: Regional Settings + save: Save + store_details: Store Details + store_legal_addres: Store Legal Address + store_settings: Store Name & URL / URI Settings + title: "New Store" diff --git a/admin/app/controllers/solidus_admin/stores_controller.rb b/admin/app/controllers/solidus_admin/stores_controller.rb index c653163d5c9..162e434392d 100644 --- a/admin/app/controllers/solidus_admin/stores_controller.rb +++ b/admin/app/controllers/solidus_admin/stores_controller.rb @@ -17,6 +17,26 @@ def index end end + def new + @store ||= Spree::Store.new + + respond_to do |format| + format.html { + render component("stores/new").new( + store: @store + ) + } + end + end + + def edit + @store = Spree::Store.find_by(id: params[:id]) + + respond_to do |format| + format.html { render component('stores/edit').new(store: @store) } + end + end + def destroy @stores = Spree::Store.where(id: params[:id]) diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 4f70c26e766..88607c30600 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -77,7 +77,7 @@ admin_resources :shipping_methods, only: [:index, :destroy] admin_resources :shipping_categories, except: [:show] admin_resources :stock_locations, only: [:index, :destroy] - admin_resources :stores, only: [:index, :destroy] + admin_resources :stores, only: [:index, :edit, :show, :destroy, :new] admin_resources :zones, only: [:index, :destroy] admin_resources :refund_reasons, except: [:show] admin_resources :reimbursement_types, only: [:index] diff --git a/admin/spec/components/solidus_admin/stores/address_form/component_spec.rb b/admin/spec/components/solidus_admin/stores/address_form/component_spec.rb new file mode 100644 index 00000000000..4382ecd2157 --- /dev/null +++ b/admin/spec/components/solidus_admin/stores/address_form/component_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::Stores::AddressForm::Component, type: :component do + let(:country) { create(:country, states_required: true) } + let(:state) { create(:state, country: country) } + let(:store) { create(:store, country: country, state: state) } + + subject(:component) { described_class.new(store: store) } + + describe "#state_options" do + context "when the country has states and requires states" do + it "returns a list of state names and IDs" do + expect(component.state_options).to include([state.name, state.id]) + end + end + + context "when the country does not require states" do + let(:country) { create(:country, states_required: false) } + + it "returns an empty array" do + expect(component.state_options).to eq([]) + end + end + + context "when there is no country assigned to the store" do + let(:store) { create(:store, country: nil) } + + it "returns an empty array" do + expect(component.state_options).to eq([]) + end + end + end +end diff --git a/admin/spec/components/solidus_admin/stores/edit/component_spec.rb b/admin/spec/components/solidus_admin/stores/edit/component_spec.rb new file mode 100644 index 00000000000..780579d7437 --- /dev/null +++ b/admin/spec/components/solidus_admin/stores/edit/component_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::Stores::Edit::Component, type: :component do + let(:store) { build(:store, id: 1, name: "Test Store") } + let(:component) { described_class.new(store: store) } + + describe "#render" do + it "renders the edit store form with existing data" do + store = Spree::Store.create!(name: "Test Store", url: "test-store.com", code: 'test-store', mail_from_address: 'test@mail.co') + render_inline described_class.new(store: store) + + expect(rendered_content).to have_selector("form") + expect(rendered_content).to have_field("store[name]", with: "Test Store") + expect(rendered_content).to have_field("store[url]", with: "test-store.com") + expect(rendered_content).to have_field("store[code]", with: "test-store") + expect(rendered_content).to have_field("store[mail_from_address]", with: "test@mail.co") + end + end + + describe "#form_id" do + it "returns a unique form id based on the store" do + expect(component.form_id).to match(/stores--edit--form-1/) + end + end + + describe "#currency_options" do + it "returns the available currencies" do + allow(Spree::Config).to receive(:available_currencies).and_return([Money::Currency.new("USD"), Money::Currency.new("EUR")]) + + expect(component.currency_options).to contain_exactly("USD", "EUR") + end + end + + describe "#cart_tax_country_options" do + it "returns available countries for cart tax selection" do + country = create(:country, name: "United States of America", iso: "US") + allow(Spree::Country).to receive(:available).and_return([country]) + + expect(component.cart_tax_country_options).to include(["United States of America", "US"]) + end + end + + describe "#localization_options" do + it "returns available locales with translated names" do + allow(Spree).to receive(:i18n_available_locales).and_return([:en, :fr]) + expect(component.localization_options).to include(["English (US)", :en]) + expect(component.localization_options).to include(["English (US)", :fr]) + end + end + + describe "#available_country_options" do + it "returns a list of available countries" do + country = create(:country, name: "United States", id: 1) + allow(Spree::Country).to receive(:order).and_return([country]) + + expect(component.available_country_options).to include(["United States", 1]) + end + end +end diff --git a/admin/spec/components/solidus_admin/stores/new/component_spec.rb b/admin/spec/components/solidus_admin/stores/new/component_spec.rb new file mode 100644 index 00000000000..300e584a225 --- /dev/null +++ b/admin/spec/components/solidus_admin/stores/new/component_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::Stores::New::Component, type: :component do + let(:store) { Spree::Store.new } + let(:component) { described_class.new(store: store) } + + describe "#render" do + it "renders the new store form" do + store = Spree::Store.new + render_inline described_class.new(store: store) + + expect(rendered_content).to have_selector("form") + expect(rendered_content).to have_field("store[name]") + expect(rendered_content).to have_field("store[url]") + end + end + + describe "#form_id" do + it "generates a unique form ID for the store" do + expect(component.form_id).to match(/--form-/) + end + end + + describe "#currency_options" do + it "returns a list of available currency ISO codes" do + allow(Spree::Config).to receive(:available_currencies).and_return([Money::Currency.new("USD"), Money::Currency.new("EUR")]) + expect(component.currency_options).to contain_exactly("USD", "EUR") + end + end + + describe "#cart_tax_country_options" do + it "returns an array of available tax country names and ISO codes" do + country = create(:country, name: "United States", iso: "US") + allow(component).to receive(:fetch_available_countries).and_return([country]) + expect(component.cart_tax_country_options).to include(["United States", "US"]) + end + end + + describe "#localization_options" do + it "returns available locales with translated names" do + allow(Spree).to receive(:i18n_available_locales).and_return([:en, :fr]) + expect(component.localization_options).to include(["English (US)", :en]) + expect(component.localization_options).to include(["English (US)", :fr]) + end + end + + describe "#available_country_options" do + it "returns a list of available countries sorted by name" do + country1 = create(:country, name: "Germany", id: 1) + country2 = create(:country, name: "France", id: 2) + allow(Spree::Country).to receive(:order).and_return([country1, country2]) + expect(component.available_country_options).to eq([["Germany", 1], ["France", 2]]) + end + end +end diff --git a/admin/spec/requests/solidus_admin/stores_spec.rb b/admin/spec/requests/solidus_admin/stores_spec.rb new file mode 100644 index 00000000000..6ed6fb71ab7 --- /dev/null +++ b/admin/spec/requests/solidus_admin/stores_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "SolidusAdmin::StoresController", type: :request do + let(:admin_user) { create(:admin_user) } + + before do + allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) + allow(admin_user).to receive(:has_spree_role?).with('admin').and_return(true) + end + + let(:resource_class) { Spree::Store } + let(:valid_attributes) { { name: "New Store", code: "new-store" } } + let(:invalid_attributes) { { name: "", code: "", domain: "" } } + + describe "GET /new" do + it "renders the new template with a 200 OK status" do + get solidus_admin.new_store_path + expect(response).to have_http_status(:ok) + end + end + + describe "GET /edit" do + let(:store) { create(:store) } + + it "renders the edit template with a 200 OK status" do + get solidus_admin.edit_store_path(store) + expect(response).to have_http_status(:ok) + end + end + + describe "Strong Parameters" do + it "permits the expected parameters" do + params = ActionController::Parameters.new(store: { store_id: 1, name: "Test Store", code: "test-store" }) + permitted_params = params.require(:store).permit(:store_id, :name, :code) + expect(permitted_params.keys).to contain_exactly("store_id", "name", "code") + end + end +end diff --git a/api/lib/spree/api_configuration.rb b/api/lib/spree/api_configuration.rb index 77b047f3258..1916cfd04ee 100644 --- a/api/lib/spree/api_configuration.rb +++ b/api/lib/spree/api_configuration.rb @@ -147,9 +147,10 @@ def promotion_attributes=(value) deprecate "promotion_attributes=" => promotion_attributes_deprecation_message, deprecator: Spree.deprecator preference :store_attributes, :array, default: [ - :id, :name, :url, :meta_description, :meta_keywords, :seo_title, - :mail_from_address, :default_currency, :code, :default, :available_locales, - :bcc_email + :id, :name, :legal_name, :url, :meta_description, :meta_keywords, :seo_title, + :mail_from_address, :default_currency, :code, :default, + :bcc_email, :contact_phone, :contact_email, :tax_id, :vat_id, :description, + :address1, :address2, :city, :zipcode, :country_id, :state_id, :state_name, :available_locales ] preference :store_credit_history_attributes, :array, default: [ diff --git a/api/openapi/solidus-api.oas.yml b/api/openapi/solidus-api.oas.yml index a59c2662b8f..f324ebdf717 100644 --- a/api/openapi/solidus-api.oas.yml +++ b/api/openapi/solidus-api.oas.yml @@ -6474,6 +6474,33 @@ components: type: string url: type: string + legal_name: + type: string + contact_email: + type: string + nullable: true + contact_phone: + type: string + description: + type: string + vat_id: + type: string + tax_id: + type: string + address1: + type: string + address2: + type: string + city: + type: string + zipcode: + type: string + state_name: + type: string + country_id: + type: integer + state_id: + type: integer taxonomy: type: object properties: @@ -6955,6 +6982,33 @@ components: type: string cart_tax_country_iso: type: string + legal_name: + type: string + contact_email: + type: string + nullable: true + contact_phone: + type: string + description: + type: string + vat_id: + type: string + tax_id: + type: string + address1: + type: string + address2: + type: string + city: + type: string + zipcode: + type: string + state_name: + type: string + country_id: + type: integer + state_id: + type: integer taxonomy-input: type: object title: Taxonomy input diff --git a/api/spec/requests/spree/api/stores_spec.rb b/api/spec/requests/spree/api/stores_spec.rb index 55b4f102416..8553ddf3d6a 100644 --- a/api/spec/requests/spree/api/stores_spec.rb +++ b/api/spec/requests/spree/api/stores_spec.rb @@ -4,6 +4,11 @@ module Spree::Api describe 'Stores', type: :request do + let(:country) { create :country, states_required: true } + let(:country_without_states) { create :country, states_required: false } + let(:state) { create :state, name: 'maryland', abbr: 'md', country: } + let!(:base_attributes) { Spree::Api::Config.store_attributes } + let!(:store) do create(:store, name: "My Spree Store", url: "spreestore.example.com") end @@ -22,6 +27,58 @@ module Spree::Api default: false) end + describe "store state validation" do + context "when store country has states_required" do + it "is invalid without a state" do + store = Spree::Store.new(name: "Test Store", country: country, state: nil, url: "spreestore.example.com", + mail_from_address: "spreestore@example.com", code: "test-store",) + expect(store).not_to be_valid + expect(store.errors[:state]).to include("can't be blank") + end + + it "is valid with a state" do + store = Spree::Store.new(name: "Test Store", country: country, state: state, url: "spreestore.example.com", + mail_from_address: "spreestore@example.com", code: "test-store",) + expect(store).to be_valid + end + end + + context "when store country has no states" do + it "is valid without a state" do + store = Spree::Store.new(name: "Test Store", country: country_without_states, state: nil, url: "spreestore.example.com", + mail_from_address: "spreestore@example.com", code: "test-store",) + expect(store).to be_valid + end + end + + it "is valid without an address and without country/state" do + expect(store).to be_valid + end + + it "is valid with only correct country and state" do + store = Spree::Store.create!( + name: "Test Store", + url: "spreestore.example.com", + mail_from_address: "spreestore.example.com", + code: "test-store", + address1: "123 Main St", + city: "New York", + zipcode: "10001", + state: state, + country: country, + ) + expect(store).to be_valid + end + end + + describe "#index" do + it "ensures the API store attributes match the expected attributes" do + get spree.api_stores_path + first_store = json_response["stores"].first + expect(first_store.keys).to include(*base_attributes.map(&:to_s)) + end + end + it "can list the available stores" do get spree.api_stores_path expect(json_response["stores"]).to match_array([ @@ -37,7 +94,20 @@ module Spree::Api "default_currency" => nil, "code" => store.code, "default" => true, - "available_locales" => ["en"] + "available_locales" => ["en"], + "legal_name" => nil, + "contact_email" => nil, + "contact_phone" => nil, + "description" => nil, + "tax_id" => nil, + "vat_id" => nil, + "address1" => nil, + "address2" => nil, + "city" => nil, + "zipcode" => nil, + "country_id" => nil, + "state_id" => nil, + "state_name" => nil }, { "id" => non_default_store.id, @@ -51,7 +121,20 @@ module Spree::Api "default_currency" => nil, "code" => non_default_store.code, "default" => false, - "available_locales" => ["en"] + "available_locales" => ["en"], + "legal_name" => nil, + "contact_email" => nil, + "contact_phone" => nil, + "description" => nil, + "tax_id" => nil, + "vat_id" => nil, + "address1" => nil, + "address2" => nil, + "city" => nil, + "zipcode" => nil, + "country_id" => nil, + "state_id" => nil, + "state_name" => nil } ]) end @@ -70,7 +153,20 @@ module Spree::Api "default_currency" => nil, "code" => store.code, "default" => true, - "available_locales" => ["en"] + "available_locales" => ["en"], + "legal_name" => nil, + "contact_email" => nil, + "contact_phone" => nil, + "description" => nil, + "tax_id" => nil, + "vat_id" => nil, + "address1" => nil, + "address2" => nil, + "city" => nil, + "zipcode" => nil, + "country_id" => nil, + "state_id" => nil, + "state_name" => nil ) end @@ -79,7 +175,14 @@ module Spree::Api code: "spree123", name: "Hack0rz", url: "spree123.example.com", - mail_from_address: "me@example.com" + mail_from_address: "me@example.com", + legal_name: 'ABC Corp', + address1: "123 Main St", + city: 'San Francisco', + country_id: country.id, + state_id: state.id, + phone: "123-456-7890", + zipcode: "12345" } post spree.api_stores_path, params: { store: store_hash } expect(response.status).to eq(201) @@ -89,13 +192,34 @@ module Spree::Api store_hash = { url: "spree123.example.com", mail_from_address: "me@example.com", - bcc_email: "bcc@example.net" + bcc_email: "bcc@example.net", + legal_name: 'XYZ Corp', + description: "Leading provider of high-quality tech accessories, offering the latest gadgets, peripherals, and electronics to enhance your digital lifestyle.", + tax_id: "TX-987654321", + vat_id: "VAT-123456789", + address1: "123 Innovation Drive", + address2: "Suite 456", + city: "New York", + country_id: country.id, + state_id: state.id, + contact_phone: "123-456-7888", + zipcode: "10001" } put spree.api_store_path(store), params: { store: store_hash } expect(response.status).to eq(200) expect(store.reload.url).to eql "spree123.example.com" expect(store.reload.mail_from_address).to eql "me@example.com" expect(store.reload.bcc_email).to eql "bcc@example.net" + expect(store.reload.legal_name).to eql "XYZ Corp" + expect(store.reload.tax_id).to eql "TX-987654321" + expect(store.reload.vat_id).to eql "VAT-123456789" + expect(store.reload.address1).to eql "123 Innovation Drive" + expect(store.reload.address2).to eql "Suite 456" + expect(store.reload.city).to eql "New York" + expect(store.reload.country_id).to eql country.id + expect(store.reload.state_id).to eql state.id + expect(store.reload.contact_phone).to eql "123-456-7888" + expect(store.reload.zipcode).to eql "10001" end context "deleting a store" do diff --git a/backend/app/views/spree/admin/stores/_address_form.html.erb b/backend/app/views/spree/admin/stores/_address_form.html.erb new file mode 100644 index 00000000000..f7e73042900 --- /dev/null +++ b/backend/app/views/spree/admin/stores/_address_form.html.erb @@ -0,0 +1,75 @@ +
+
+
+ <%= f.field_container :legal_name do %> + <%= f.label :legal_name %> + <%= f.text_field :legal_name, class: 'form-control' %> + <% end %> +
+
+ <%= f.field_container :city do %> + <%= f.label :city %> + <%= f.text_field :city, class: 'form-control' %> + <% end %> +
+
+ <%= f.field_container :address1 do %> + <%= f.label :address1 %> + <%= f.text_field :address1, class: 'form-control' %> + <% end %> +
+
+ <%= f.field_container :zipcode do %> + <%= f.label :zipcode %> + <%= f.text_field :zipcode, class: 'form-control' %> + <% end %> +
+
+ <%= f.field_container :address2 do %> + <%= f.label :address2 %> + <%= f.text_field :address2, class: 'fullwidth' %> + <% end %> +
+
+ <%= f.field_container :country_id do %> + <%= f.label :country_id, Spree::Country.model_name.human %> + + <%= f.collection_select :country_id, available_countries, :id, :name, { include_blank: true }, {class: 'custom-select fullwidth js-country_id'} %> + + <% end %> +
+
+ <%= f.label :state_id, Spree::State.model_name.human %> + + <%= f.hidden_field :state_name, value: nil %> + <% states = f.object.country.try(:states).nil? ? [] : f.object.country.states %> + <%= f.text_field :state_name, + style: "display: #{states.empty? ? 'block' : 'none' };", + disabled: !states.empty?, class: 'fullwidth state_name js-state_name' %> + <%= f.hidden_field :state_id, value: nil %> + <%= f.collection_select :state_id, + states.sort, + :id, :name, + { include_blank: true }, + { class: 'custom-select fullwidth js-state_id', + style: "display: #{states.empty? ? 'none' : 'block' };", + disabled: states.empty? } %> + +
+
+ <%= f.field_container :tax_id do %> + <%= f.label :tax_id %> + <%= f.text_field :tax_id, class: 'fullwidth' %> + <%= f.error_message_on :tax_id %> + <% end %> +
+
+ <%= f.field_container :vat_id do %> + <%= f.label :vat_id %> + <%= f.field_hint :vat_id %> + <%= f.text_field :vat_id, class: 'fullwidth' %> + <%= f.error_message_on :vat_id %> + <% end %> +
+
+
diff --git a/backend/app/views/spree/admin/stores/_form.html.erb b/backend/app/views/spree/admin/stores/_form.html.erb index 59dc4a4e899..80084fefecd 100644 --- a/backend/app/views/spree/admin/stores/_form.html.erb +++ b/backend/app/views/spree/admin/stores/_form.html.erb @@ -1,85 +1,146 @@ -
-
- <%= f.field_container :name do %> - <%= f.label :name, class: 'required' %> - <%= f.text_field :name, required: true, class: 'fullwidth' %> - <%= f.error_message_on :name %> - <% end %> +
+ +

<%= t('.store_settings') %>

+
+
+ <%= f.field_container :name do %> + <%= f.label :name, class: 'required' %> + <%= f.text_field :name, required: true, class: 'fullwidth' %> + <%= f.error_message_on :name %> + <% end %> +
- <%= f.field_container :code do %> - <%= f.label :code, class: 'required' %> - <%= f.field_hint :code %> - <%= f.text_field :code, required: true, class: 'fullwidth' %> - <%= f.error_message_on :code %> - <% end %> +
+ <%= f.field_container :url do %> + <%= f.label :url, class: 'required' %> + <%= f.text_field :url, required: true, class: 'fullwidth' %> + <%= f.error_message_on :url %> + <% end %> +
- <%= f.field_container :seo_title do %> - <%= f.label :seo_title %> - <%= f.text_field :seo_title, class: 'fullwidth' %> - <%= f.error_message_on :seo_title %> - <% end %> - - <%= f.field_container :meta_keywords do %> - <%= f.label :meta_keywords %> - <%= f.text_field :meta_keywords, class: 'fullwidth' %> - <%= f.error_message_on :meta_keywords %> - <% end %> - - <%= f.field_container :meta_description do %> - <%= f.label :meta_description %> - <%= f.text_area :meta_description, class: 'fullwidth' %> - <%= f.error_message_on :meta_description %> - <% end %> +
+ <%= f.field_container :code do %> + <%= f.label :code, class: 'required' %> + <%= f.field_hint :code %> + <%= f.text_field :code, required: true, class: 'fullwidth' %> + <%= f.error_message_on :code %> + <% end %> +
-
- <%= f.field_container :url do %> - <%= f.label :url, class: 'required' %> - <%= f.text_field :url, required: true, class: 'fullwidth' %> - <%= f.error_message_on :url %> - <% end %> - <%= f.field_container :mail_from_address do %> - <%= f.label :mail_from_address, class: 'required' %> - <%= f.text_field :mail_from_address, required: true, class: 'fullwidth' %> - <%= f.error_message_on :mail_from_address %> - <% end %> + +

<%= t('.regional_settings') %>

+
+
+ <%= f.field_container :default_currency do %> + <%= f.label :default_currency %> + <%= f.field_hint :default_currency %> + <%= f.select :default_currency, + Spree::Config.available_currencies.map(&:iso_code), + { include_blank: true }, + { class: 'custom-select fullwidth' } %> + <%= f.error_message_on :default_currency %> + <% end %> +
+
+ <%= f.field_container :cart_tax_country_iso do %> + <%= f.label :cart_tax_country_iso %> + <%= f.field_hint :cart_tax_country_iso %> + <%= f.collection_select :cart_tax_country_iso, + available_countries(restrict_to_zone: nil), :iso, :name, + { include_blank: t(".no_cart_tax_country") }, + { class: "custom-select fullwidth" } %> + <%= f.error_message_on :cart_tax_country_iso %> + <% end %> +
+
+ <%= f.field_container :available_locales do %> + <%= f.label :available_locales %> + <%= f.field_hint :available_locales %> + <%= f.select :available_locales, + Spree.i18n_available_locales.map { |locale| + [I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s, fallback: false), locale] + }.sort, + { }, + { class: 'select2 fullwidth', multiple: true } %> + <%= f.error_message_on :default_currency %> + <% end %> +
+
- <%= f.field_container :bcc_email do %> - <%= f.label :bcc_email %> - <%= f.text_field :bcc_email, class: 'fullwidth' %> - <%= f.error_message_on :bcc_email %> - <% end %> + +

<%= t('.email_settings') %>

+
+
+ <%= f.field_container :mail_from_address do %> + <%= f.label :mail_from_address, class: 'required' %> + <%= f.text_field :mail_from_address, required: true, class: 'fullwidth' %> + <%= f.error_message_on :mail_from_address %> + <% end %> +
+
+ <%= f.field_container :bcc_email do %> + <%= f.label :bcc_email %> + <%= f.text_field :bcc_email, class: 'fullwidth' %> + <%= f.error_message_on :bcc_email %> + <% end %> +
+
- <%= f.field_container :default_currency do %> - <%= f.label :default_currency %> - <%= f.field_hint :default_currency %> - <%= f.select :default_currency, - Spree::Config.available_currencies.map(&:iso_code), - { include_blank: true }, - { class: 'custom-select fullwidth' } %> - <%= f.error_message_on :default_currency %> - <% end %> + +

<%= t('.store_legal_addres') %>

+
+ <%= render partial: 'address_form', locals: { f: f, type: 'store' } %> +
- <%= f.field_container :cart_tax_country_iso do %> - <%= f.label :cart_tax_country_iso %> - <%= f.field_hint :cart_tax_country_iso %> - <%= f.collection_select :cart_tax_country_iso, - available_countries(restrict_to_zone: nil), :iso, :name, - { include_blank: t(".no_cart_tax_country") }, - { class: "custom-select fullwidth" } %> - <%= f.error_message_on :cart_tax_country_iso %> - <% end %> + +

<%= t('.contact_options') %>

+
+
+ <%= f.field_container :contact_phone do %> + <%= f.label :contact_phone %> + <%= f.phone_field :contact_phone, class: 'fullwidth' %> + <%= f.error_message_on :contact_phone %> + <% end %> +
+
+ <%= f.field_container :contact_email do %> + <%= f.label :contact_email %> + <%= f.email_field :contact_email, class: 'form-control' %> + <%= f.error_message_on :contact_email %> + <% end %> +
+
- <%= f.field_container :available_locales do %> - <%= f.label :available_locales %> - <%= f.field_hint :available_locales %> - <%= f.select :available_locales, - Spree.i18n_available_locales.map { |locale| - [I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s, fallback: false), locale] - }.sort, - { }, - { class: 'select2 fullwidth', multiple: true } %> - <%= f.error_message_on :default_currency %> - <% end %> + +

<%= t('.content_on_storefront') %>

+
+
+ <%= f.field_container :seo_title do %> + <%= f.label :seo_title %> + <%= f.text_field :seo_title, class: 'fullwidth' %> + <%= f.error_message_on :seo_title %> + <% end %> +
+
+ <%= f.field_container :meta_keywords do %> + <%= f.label :meta_keywords %> + <%= f.text_field :meta_keywords, class: 'fullwidth' %> + <%= f.error_message_on :meta_keywords %> + <% end %> +
+
+ <%= f.field_container :description do %> + <%= f.label :description, class: 'form-label' %> + <%= f.text_area :description, class: 'form-control' %> + <% end %> +
+
+ <%= f.field_container :meta_description do %> + <%= f.label :meta_description %> + <%= f.text_area :meta_description, class: 'fullwidth' %> + <%= f.error_message_on :meta_description %> + <% end %> +
diff --git a/core/app/models/spree/store.rb b/core/app/models/spree/store.rb index 9350fb0a2b8..462c55682cd 100644 --- a/core/app/models/spree/store.rb +++ b/core/app/models/spree/store.rb @@ -15,12 +15,16 @@ class Store < Spree::Base has_many :store_shipping_methods, inverse_of: :store has_many :shipping_methods, through: :store_shipping_methods + belongs_to :state, class_name: 'Spree::State', optional: true + belongs_to :country, class_name: 'Spree::Country', optional: true + has_many :orders, class_name: "Spree::Order" validates :code, presence: true, uniqueness: { allow_blank: true, case_sensitive: true } validates :name, presence: true validates :url, presence: true validates :mail_from_address, presence: true + validates :state, presence: true, if: -> { country&.states_required } self.allowed_ransackable_attributes = %w[name url code] diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index 0dd3145a750..b66e0c6dc2c 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -354,18 +354,32 @@ en: quantity: Quantity variant: Variant spree/store: - available_locales: Locales Available in the Storefront + address: Address + address1: Street Address + address2: Street Address (cont'd) + available_locales: Languages Available in the Storefront bcc_email: BCC Email cart_tax_country_iso: Tax Country for Empty Carts + city: City code: Slug + contact_email: Contact Email + contact_phone: Contact Phone + country_id: Country default: Default default_currency: Default Currency + description: Store Description + legal_name: Legal Name mail_from_address: Mail From Address meta_description: Meta Description meta_keywords: Meta Keywords name: Site Name + postal_code: Postal Code seo_title: Seo Title + state_id: State + tax_id: Tax-ID url: Site URL + vat_id: VAT-ID + zipcode: Zip Code spree/store_credit: amount: Amount amount_authorized: Amount Authorized @@ -939,7 +953,13 @@ en: view: View store credit stores: form: + contact_options: Contact Options + content_on_storefront: Content on Storefront + email_settings: Email Settings no_cart_tax_country: No taxes on carts without address + regional_settings: Regional Settings + store_legal_addres: Store Legal Address + store_settings: Store Name & URL / URI Settings tab: checkout: Refunds and Returns configuration: Configuration @@ -1629,6 +1649,7 @@ en: cart_tax_country_iso: 'This determines which country is used for taxes on carts (orders which don''t yet have an address).
Default: None.' code: An identifier for your store. Developers may need this value if you operate multiple storefronts. default_currency: This determines which currency will be used for the storefront's product prices. Please, be aware that changing this configuration, only products that have prices in the selected currency will be listed on your storefront.

This setting won't change the default currency used when you create a product. For that, only the global `Spree::Config.currency` is taken into account. + vat_id: Enter your VAT-ID without the Country Prefix (eg IT for Italy or DE for Germany) but solely the identification string. spree/tax_category: is_default: When checked, this tax category will be selected by default when creating new products or variants. spree/tax_rate: diff --git a/core/db/migrate/20250202173007_add_store_attributes_to_spree_stores.rb b/core/db/migrate/20250202173007_add_store_attributes_to_spree_stores.rb new file mode 100644 index 00000000000..f1a854f2b7b --- /dev/null +++ b/core/db/migrate/20250202173007_add_store_attributes_to_spree_stores.rb @@ -0,0 +1,19 @@ +class AddStoreAttributesToSpreeStores < ActiveRecord::Migration[7.2] + def change + add_column :spree_stores, :legal_name, :string + add_column :spree_stores, :contact_email, :string + add_column :spree_stores, :description, :text + add_column :spree_stores, :vat_id, :string + add_column :spree_stores, :tax_id, :string + add_column :spree_stores, :address1, :string + add_column :spree_stores, :address2, :string + add_column :spree_stores, :city, :string + add_column :spree_stores, :zipcode, :string + add_column :spree_stores, :state_name, :string + add_column :spree_stores, :contact_phone, :string + add_column :spree_stores, :country_id, :integer + add_column :spree_stores, :state_id, :integer + add_index :spree_stores, :country_id + add_index :spree_stores, :state_id + end +end diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb index cd59e762e21..eecb914fd57 100644 --- a/core/lib/spree/permitted_attributes.rb +++ b/core/lib/spree/permitted_attributes.rb @@ -121,10 +121,12 @@ module PermittedAttributes :quantity, :stock_item, :stock_item_id, :originator, :action ] - @@store_attributes = [:name, :url, :seo_title, :meta_keywords, + @@store_attributes = [:name, :legal_name, :url, :seo_title, :meta_keywords, :meta_description, :default_currency, :mail_from_address, :cart_tax_country_iso, - :bcc_email] + :bcc_email, :contact_email, :contact_phone, :code, + :tax_id, :vat_id, :description, :address1, :address2, + :city, :zipcode, :country_id, :state_id, :state_name] @@taxonomy_attributes = [:name]