diff --git a/app/controllers/spree/api/v2/platform/clone_stores_controller.rb b/app/controllers/spree/api/v2/platform/clone_stores_controller.rb index 3c3be36..c70a2d3 100644 --- a/app/controllers/spree/api/v2/platform/clone_stores_controller.rb +++ b/app/controllers/spree/api/v2/platform/clone_stores_controller.rb @@ -5,6 +5,8 @@ module Platform class CloneStoresController < Spree::Olitt::CloneStore::CloneStoreController skip_forgery_protection + attr_reader :current_api_key + rescue_from Doorkeeper::Errors::DoorkeeperError, with: :render_unauthorized rescue_from ActiveRecord::RecordNotUnique, with: :render_bad_request_exception rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid @@ -25,11 +27,17 @@ def show private def authorize_clone_store_request! + if api_key_header_present? + authenticate_secret_key! + return + end + scopes = action_name == 'show' ? %i[read admin] : %i[write admin] doorkeeper_authorize!(*scopes) end def authorize_superadmin_user_token! + return if current_api_key.present? return if spree_current_user.nil? return if superuser_with_global_admin_role?(spree_current_user) @@ -45,11 +53,39 @@ def render_unauthorized(_exception) end def validate_token_client + return if api_key_header_present? return if doorkeeper_token.nil? raise Doorkeeper::Errors::DoorkeeperError if doorkeeper_token.application.nil? end + def authenticate_secret_key! + @current_api_key = Spree::ApiKey.find_by(token_digest: Spree::ApiKey.compute_token_digest(extract_api_key)) + @current_api_key = nil if @current_api_key && (current_store.blank? || @current_api_key.store_id != current_store.id) + + unless @current_api_key + render_api_error('Valid secret API key required', :unauthorized) + return false + end + + touch_api_key_if_needed(@current_api_key) + true + end + + def touch_api_key_if_needed(api_key) + return if api_key.last_used_at.present? && api_key.last_used_at > 1.hour.ago + + Spree::ApiKeys::MarkAsUsed.perform_later(api_key.id, Time.current) + end + + def extract_api_key + request.headers['X-Spree-Api-Key'].presence + end + + def api_key_header_present? + extract_api_key.present? + end + def render_api_error(message, status) render json: { error: message }, status: status end diff --git a/app/controllers/spree/olitt/clone_store/clone_store_controller.rb b/app/controllers/spree/olitt/clone_store/clone_store_controller.rb index 9b7743b..ce3420b 100644 --- a/app/controllers/spree/olitt/clone_store/clone_store_controller.rb +++ b/app/controllers/spree/olitt/clone_store/clone_store_controller.rb @@ -26,8 +26,10 @@ def render_error(duplicator:) def handle_create_vendor(email, password, password_confirmation) user_email = email.to_s.strip.downcase @vendor = find_or_create_vendor(user_email) - user = find_or_create_user(user_email, password, password_confirmation) - assign_vendor_role(user, @vendor) + legacy_user = existing_legacy_user(user_email) + admin_user = find_or_create_admin_user(user_email, password, password_confirmation, legacy_user: legacy_user) + assign_vendor_role(admin_user, @vendor) + link_admin_user_to_vendor!(vendor: @vendor, admin_user: admin_user, legacy_user: legacy_user) activate_vendor(@vendor) end @@ -165,27 +167,59 @@ def find_or_create_vendor(email) ) end - def find_or_create_user(email, password, password_confirmation) - user = Spree.user_class.find_or_initialize_by(email: email) - return user if user.persisted? + def find_or_create_admin_user(email, password, password_confirmation, legacy_user: nil) + admin_user = Spree.admin_user_class.find_or_initialize_by(email: email) + return admin_user if admin_user.persisted? - user.password = password - user.password_confirmation = password_confirmation.presence || password - user.save! - user + admin_user.login ||= email if admin_user.respond_to?(:login=) + admin_user.password = password + admin_user.password_confirmation = password_confirmation.presence || password if admin_user.respond_to?(:password_confirmation=) + + if legacy_user.present? + admin_user.first_name ||= legacy_user.first_name if admin_user.respond_to?(:first_name=) + admin_user.last_name ||= legacy_user.last_name if admin_user.respond_to?(:last_name=) + admin_user.selected_locale ||= legacy_user.selected_locale if admin_user.respond_to?(:selected_locale=) + end + + admin_user.save! + admin_user end - def assign_vendor_role(user, vendor) + def assign_vendor_role(admin_user, vendor) vendor_role_name = defined?(Spree::Vendor::DEFAULT_VENDOR_ROLE) ? Spree::Vendor::DEFAULT_VENDOR_ROLE : 'vendor' - vendor_role = Spree::Role.find_or_create_by!(name: vendor_role_name) + vendor_role = vendor.respond_to?(:default_user_role) ? (vendor.default_user_role || Spree::Role.find_or_create_by!(name: vendor_role_name)) : Spree::Role.find_or_create_by!(name: vendor_role_name) Spree::RoleUser.find_or_create_by!( - user: user, + user: admin_user, role: vendor_role, resource: vendor ) end + def existing_legacy_user(email) + return nil if Spree.user_class == Spree.admin_user_class + + Spree.user_class.find_by(email: email) + end + + def link_admin_user_to_vendor!(vendor:, admin_user:, legacy_user: nil) + return unless defined?(Spree::VendorUser) + return unless ActiveRecord::Base.connection.data_source_exists?('spree_vendor_users') + + vendor_user = if legacy_user.present? + Spree::VendorUser.find_by(vendor_id: vendor.id, user_id: legacy_user.id) + end + + if vendor_user.nil? && Spree::VendorUser.column_names.include?('admin_user_id') + vendor_user = Spree::VendorUser.find_by(vendor_id: vendor.id, admin_user_id: admin_user.id) + end + + vendor_user ||= Spree::VendorUser.new(vendor_id: vendor.id) + vendor_user.user_id = legacy_user.id if legacy_user.present? && vendor_user.respond_to?(:user_id=) && vendor_user.user_id.blank? + vendor_user.admin_user_id = admin_user.id if vendor_user.respond_to?(:admin_user_id=) + vendor_user.save! if vendor_user.new_record? || vendor_user.changed? + end + def activate_vendor(vendor) return if %w[active approved].include?(vendor.state) diff --git a/app/helpers/spree/olitt/clone_store/clone_store_helpers.rb b/app/helpers/spree/olitt/clone_store/clone_store_helpers.rb index d4cf209..9dbf194 100644 --- a/app/helpers/spree/olitt/clone_store/clone_store_helpers.rb +++ b/app/helpers/spree/olitt/clone_store/clone_store_helpers.rb @@ -18,7 +18,7 @@ def vendor_params end def source_id_param - clone_store_payload.require(:source_store_id) + clone_store_payload[:source_store_id].presence || Spree::Store.default&.id end def resource_serializer diff --git a/app/helpers/spree/olitt/clone_store/product_helpers.rb b/app/helpers/spree/olitt/clone_store/product_helpers.rb index 83ee1ca..1d3b476 100644 --- a/app/helpers/spree/olitt/clone_store/product_helpers.rb +++ b/app/helpers/spree/olitt/clone_store/product_helpers.rb @@ -41,7 +41,7 @@ def assign_variant_prices(new_variant:, old_variant:) if prices.any? && new_variant.respond_to?(:prices=) new_variant.prices = prices - elsif old_variant.respond_to?(:price) && new_variant.respond_to?(:price=) + elsif old_variant.respond_to?(:price) && new_variant.respond_to?(:price=) && old_variant.price.present? new_variant.price = old_variant.price new_variant.currency = old_variant.currency if new_variant.respond_to?(:currency=) && old_variant.respond_to?(:currency) end diff --git a/app/models/spree/admin_user_decorator.rb b/app/models/spree/admin_user_decorator.rb new file mode 100644 index 0000000..5edf619 --- /dev/null +++ b/app/models/spree/admin_user_decorator.rb @@ -0,0 +1,13 @@ +module Spree + module AdminUserDecorator + def self.prepended(base) + base.has_many :clone_requests, + class_name: 'Spree::Olitt::CloneStore::CloneRequest', + foreign_key: :user_id, + inverse_of: :admin_user, + dependent: :nullify + end + end +end + +Spree.admin_user_class.prepend(Spree::AdminUserDecorator) unless Spree.admin_user_class < Spree::AdminUserDecorator \ No newline at end of file diff --git a/app/models/spree/olitt/clone_store/clone_request.rb b/app/models/spree/olitt/clone_store/clone_request.rb index cad2ea7..ceec195 100644 --- a/app/models/spree/olitt/clone_store/clone_request.rb +++ b/app/models/spree/olitt/clone_store/clone_request.rb @@ -18,11 +18,19 @@ class CloneRequest < ::ApplicationRecord belongs_to :source_store, class_name: 'Spree::Store' belongs_to :store, class_name: 'Spree::Store', optional: true belongs_to :vendor, class_name: 'Spree::Vendor', optional: true - belongs_to :user, class_name: Spree.user_class.to_s, optional: true + belongs_to :admin_user, class_name: Spree.admin_user_class.to_s, foreign_key: :user_id, optional: true belongs_to :role_user, class_name: 'Spree::RoleUser', optional: true validates :store_name, :store_url, :store_code, :store_mail_from_address, :vendor_email, :vendor_password, presence: true + def user + admin_user + end + + def user=(value) + self.admin_user = value + end + def mark_enqueued!(job) update!( job_id: job.job_id, @@ -48,7 +56,7 @@ def mark_failed!(message) def cleanup_failed_clone! cloned_store = store assigned_role_user = created_role_user? ? role_user : nil - assigned_user = created_user? ? user : nil + assigned_admin_user = created_user? ? admin_user : nil assigned_vendor = created_vendor? ? vendor : nil update_columns(store_id: nil, role_user_id: nil, user_id: nil, vendor_id: nil) @@ -56,9 +64,9 @@ def cleanup_failed_clone! cloned_store&.destroy! assigned_role_user&.destroy! - if assigned_user.present? - assigned_user.reload - assigned_user.destroy! if assigned_user.role_users.reload.none? + if assigned_admin_user.present? + assigned_admin_user.reload + assigned_admin_user.destroy! if assigned_admin_user.role_users.reload.none? end if assigned_vendor.present? diff --git a/app/services/spree/olitt/clone_store/clone_request_creator.rb b/app/services/spree/olitt/clone_store/clone_request_creator.rb index 89f6e83..72c6e59 100644 --- a/app/services/spree/olitt/clone_store/clone_request_creator.rb +++ b/app/services/spree/olitt/clone_store/clone_request_creator.rb @@ -14,7 +14,7 @@ def initialize(source_store_id:, store_params:, vendor_params:) def call validate_vendor_params! - source_store = Spree::Store.find(@source_store_id) + source_store = resolved_source_store CloneRequest.create!( source_store: source_store, @@ -64,6 +64,13 @@ def extract_record_not_unique_message(error) raw_message = error.cause&.message.presence || error.message raw_message.to_s.sub(/\AMysql2::Error:\s*/i, '') end + + def resolved_source_store + return @source_store if @source_store.present? + + source_store_id = @source_store_id.presence || Spree::Store.default&.id + Spree::Store.find(source_store_id) + end end end end diff --git a/app/services/spree/olitt/clone_store/clone_request_presenter.rb b/app/services/spree/olitt/clone_store/clone_request_presenter.rb index 4696c50..3819d9a 100644 --- a/app/services/spree/olitt/clone_store/clone_request_presenter.rb +++ b/app/services/spree/olitt/clone_store/clone_request_presenter.rb @@ -19,7 +19,10 @@ def as_json private def resource_payload - return @clone_request.fallback_store_payload if @clone_request.store.blank? + if @clone_request.store.blank? + fallback_payload = @clone_request.fallback_store_payload + return fallback_payload.is_a?(Hash) ? fallback_payload.fetch(:data, fallback_payload['data']) : fallback_payload + end serialized = @serializer.call(@clone_request.store) serialized.is_a?(Hash) ? serialized.fetch(:data, serialized['data']) : serialized @@ -34,9 +37,24 @@ def metadata status: @clone_request.status, source_store_id: @clone_request.source_store_id, queue_name: @clone_request.queue_name, - queued_at: @clone_request.enqueued_at + queued_at: @clone_request.enqueued_at, + vendor: vendor_metadata } end + + def vendor_metadata + { + vendor_id: @clone_request.vendor_id, + vendor_slug: @clone_request.vendor&.slug, + admin_user_id: @clone_request.admin_user&.id, + email: @clone_request.vendor_email, + next_path: admin_dashboard_path + } + end + + def admin_dashboard_path + '/admin' + end end end end diff --git a/app/services/spree/olitt/clone_store/clone_request_provisioner.rb b/app/services/spree/olitt/clone_store/clone_request_provisioner.rb index bfbe7a7..9a1d5e9 100644 --- a/app/services/spree/olitt/clone_store/clone_request_provisioner.rb +++ b/app/services/spree/olitt/clone_store/clone_request_provisioner.rb @@ -12,9 +12,15 @@ def initialize(clone_request:) def call ActiveRecord::Base.transaction do source_store = @clone_request.source_store + legacy_user = existing_legacy_user(@clone_request.vendor_email) vendor, created_vendor = find_or_create_vendor(@clone_request.vendor_email) - user, created_user = find_or_create_user(@clone_request.vendor_email, @clone_request.vendor_password) - role_user, created_role_user = assign_vendor_role(user, vendor) + admin_user, created_user = find_or_create_admin_user( + @clone_request.vendor_email, + @clone_request.vendor_password, + legacy_user: legacy_user + ) + role_user, created_role_user = assign_vendor_role(admin_user, vendor) + link_admin_user_to_vendor!(vendor: vendor, admin_user: admin_user, legacy_user: legacy_user) activate_vendor(vendor) @@ -24,7 +30,7 @@ def call @clone_request.update!( store: store, vendor: vendor, - user: user, + admin_user: admin_user, role_user: role_user, created_vendor: @clone_request.created_vendor? || created_vendor, created_user: @clone_request.created_user? || created_user, @@ -84,38 +90,82 @@ def find_or_create_vendor(email) raise end - def find_or_create_user(email, password) - user = existing_user(email) || Spree.user_class.find_or_initialize_by(email: email) - revive_user!(user) if user.present? - return [user, false] if user.persisted? - - user.password = password - user.password_confirmation = password - user.save! + def find_or_create_admin_user(email, password, legacy_user: nil) + admin_user = build_admin_user(email) + revive_admin_user!(admin_user) if admin_user.present? + return [admin_user, false] if admin_user.persisted? - [user, true] + configure_admin_user(admin_user, email, password, legacy_user) + persist_admin_user(admin_user) rescue ActiveRecord::RecordNotUnique - user = existing_user(email) - revive_user!(user) if user.present? - return [user, false] if user.present? + recover_existing_admin_user(email) + end + + def build_admin_user(email) + existing_admin_user(email) || Spree.admin_user_class.find_or_initialize_by(email: email) + end + + def configure_admin_user(admin_user, email, password, legacy_user) + admin_user.login ||= email if admin_user.respond_to?(:login=) + admin_user.password = password + admin_user.password_confirmation = password if admin_user.respond_to?(:password_confirmation=) + copy_legacy_admin_user_attributes(admin_user, legacy_user) + end + + def copy_legacy_admin_user_attributes(admin_user, legacy_user) + return if legacy_user.blank? + + admin_user.first_name ||= legacy_user.first_name if admin_user.respond_to?(:first_name=) + admin_user.last_name ||= legacy_user.last_name if admin_user.respond_to?(:last_name=) + admin_user.selected_locale ||= legacy_user.selected_locale if admin_user.respond_to?(:selected_locale=) + end + + def persist_admin_user(admin_user) + admin_user.save! + + [admin_user, true] + end + + def recover_existing_admin_user(email) + admin_user = existing_admin_user(email) + revive_admin_user!(admin_user) if admin_user.present? + return [admin_user, false] if admin_user.present? raise end - def assign_vendor_role(user, vendor) + def assign_vendor_role(admin_user, vendor) vendor_role_name = defined?(Spree::Vendor::DEFAULT_VENDOR_ROLE) ? Spree::Vendor::DEFAULT_VENDOR_ROLE : 'vendor' - vendor_role = Spree::Role.find_or_create_by!(name: vendor_role_name) - role_user = Spree::RoleUser.find_by(user: user, role: vendor_role, resource: vendor) + vendor_role = vendor.respond_to?(:default_user_role) ? (vendor.default_user_role || Spree::Role.find_or_create_by!(name: vendor_role_name)) : Spree::Role.find_or_create_by!(name: vendor_role_name) + role_user = Spree::RoleUser.find_by(user: admin_user, role: vendor_role, resource: vendor) return [role_user, false] if role_user.present? - [Spree::RoleUser.create!(user: user, role: vendor_role, resource: vendor), true] + [Spree::RoleUser.create!(user: admin_user, role: vendor_role, resource: vendor), true] rescue ActiveRecord::RecordNotUnique - role_user = Spree::RoleUser.find_by(user: user, role: vendor_role, resource: vendor) + role_user = Spree::RoleUser.find_by(user: admin_user, role: vendor_role, resource: vendor) return [role_user, false] if role_user.present? raise end + def link_admin_user_to_vendor!(vendor:, admin_user:, legacy_user: nil) + return unless defined?(Spree::VendorUser) + return unless ActiveRecord::Base.connection.data_source_exists?('spree_vendor_users') + + vendor_user = if legacy_user.present? + Spree::VendorUser.find_by(vendor_id: vendor.id, user_id: legacy_user.id) + end + + if vendor_user.nil? && Spree::VendorUser.column_names.include?('admin_user_id') + vendor_user = Spree::VendorUser.find_by(vendor_id: vendor.id, admin_user_id: admin_user.id) + end + + vendor_user ||= Spree::VendorUser.new(vendor_id: vendor.id) + vendor_user.user_id = legacy_user.id if legacy_user.present? && vendor_user.respond_to?(:user_id=) && vendor_user.user_id.blank? + vendor_user.admin_user_id = admin_user.id if vendor_user.respond_to?(:admin_user_id=) + vendor_user.save! if vendor_user.new_record? || vendor_user.changed? + end + def activate_vendor(vendor) return if %w[active approved].include?(vendor.state) @@ -127,7 +177,13 @@ def existing_vendor(email) ::Spree::Vendor.unscoped.find_by(notification_email: email) || ::Spree::Vendor.unscoped.find_by(name: email) end - def existing_user(email) + def existing_admin_user(email) + Spree.admin_user_class.unscoped.find_by(email: email) + end + + def existing_legacy_user(email) + return nil if Spree.user_class == Spree.admin_user_class + Spree.user_class.unscoped.find_by(email: email) end @@ -142,10 +198,10 @@ def revive_vendor!(vendor) vendor.update_columns(deleted_at: nil, updated_at: Time.current) end - def revive_user!(user) - return if user.blank? || !user.respond_to?(:deleted_at) || user.deleted_at.blank? + def revive_admin_user!(admin_user) + return if admin_user.blank? || !admin_user.respond_to?(:deleted_at) || admin_user.deleted_at.blank? - user.update_columns(deleted_at: nil, updated_at: Time.current) + admin_user.update!(deleted_at: nil, updated_at: Time.current) end end end diff --git a/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb b/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb new file mode 100644 index 0000000..359ec55 --- /dev/null +++ b/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Spree::Api::V2::Platform::CloneStoresController, type: :controller do + routes { Spree::Core::Engine.routes } + + let!(:source_store) do + create(:store, default: false, name: 'Source Store', url: 'source.example.com', code: 'source-store') + end + + let!(:product) do + create(:product, name: 'Cloned Product', slug: 'cloned-product', stores: [source_store]) + end + + let(:clone_params) do + { + clone_store: { + source_store_id: source_store.id, + store: { + name: 'Clone Store', + url: 'clone.example.com', + code: 'clone-store', + mail_from_address: 'clone@example.com' + }, + vendor: { + email: 'clone.vendor@example.com', + password: 'Password123!', + password_confirmation: 'Password123!' + } + } + } + end + + before do + ActiveJob::Base.queue_adapter = :test + allow(controller).to receive(:validate_token_client).and_return(true) + allow(controller).to receive(:authorize_clone_store_request!).and_return(true) + allow(controller).to receive(:authorize_superadmin_user_token!).and_return(true) + end + + after do + ActiveJob::Base.queue_adapter = :test + end + + it 'clones a store with products and returns vendor metadata' do + post :create, params: clone_params, format: :json + + expect(response).to have_http_status(:accepted) + + payload = JSON.parse(response.body) + clone_request = Spree::Olitt::CloneStore::CloneRequest.find(payload.fetch('clone_request_id')) + + expect(clone_request).to be_queued + + Spree::Olitt::CloneStore::CloneStoreJob.perform_now(clone_request.id) + clone_request.reload + + expect(clone_request).to be_completed + expect(clone_request.store).to be_present + expect(clone_request.store.products.count).to eq(1) + expect(clone_request.store.products.first.name).to eq('Cloned Product') + + vendor_meta = payload.fetch('meta').fetch('vendor') + expect(vendor_meta).to include( + 'email' => 'clone.vendor@example.com', + 'next_path' => '/admin' + ) + end + + describe 'secret API key authentication' do + let(:api_key) { instance_double('Spree::ApiKey', id: 44, store_id: source_store.id, last_used_at: nil) } + + it 'accepts X-Spree-Api-Key without doorkeeper auth' do + request.headers['X-Spree-Api-Key'] = 'sk_test_123' + + allow(controller).to receive(:current_store).and_return(source_store) + allow(Spree::ApiKey).to receive(:find_by_secret_token).with('sk_test_123').and_return(api_key) + allow(Spree::ApiKeys::MarkAsUsed).to receive(:perform_later) + expect(controller).not_to receive(:doorkeeper_authorize!) + + controller.send(:authorize_clone_store_request!) + + expect(controller.send(:current_api_key)).to eq(api_key) + end + + it 'renders unauthorized for an invalid X-Spree-Api-Key' do + request.headers['X-Spree-Api-Key'] = 'sk_invalid' + + allow(controller).to receive(:current_store).and_return(source_store) + allow(Spree::ApiKey).to receive(:find_by_secret_token).with('sk_invalid').and_return(nil) + + controller.send(:authorize_clone_store_request!) + + expect(response).to have_http_status(:unauthorized) + expect(JSON.parse(response.body)).to eq('error' => 'Valid secret API key required') + end + end +end \ No newline at end of file diff --git a/spec/controllers/spree/olitt/clone_store/clone_store_controller_spec.rb b/spec/controllers/spree/olitt/clone_store/clone_store_controller_spec.rb index d5e5c4a..7ffa55f 100644 --- a/spec/controllers/spree/olitt/clone_store/clone_store_controller_spec.rb +++ b/spec/controllers/spree/olitt/clone_store/clone_store_controller_spec.rb @@ -39,6 +39,13 @@ class DummyCloneStoreController < Spree::Olitt::CloneStore::CloneStoreController it 'I can get source store id' do expect(controller.source_id_param).to eq(store.id.to_s) end + + it 'falls back to the default store when source_store_id is missing' do + allow(params).to receive(:[]).with(:clone_store).and_return(ActionController::Parameters.new({ store: { name: 'Local Test Store', url: 'local.test', code: 'local_test', mail_from_address: 'spree@example.com' }, vendor: { email: 'vendor@example.com', password: 'secret123', password_confirmation: 'secret123' } })) + allow(Spree::Store).to receive(:default).and_return(store) + + expect(controller.source_id_param).to eq(store.id) + end end # Store diff --git a/spec/helpers/spree/olitt/clone_store/product_helpers_spec.rb b/spec/helpers/spree/olitt/clone_store/product_helpers_spec.rb index caab429..b685db0 100644 --- a/spec/helpers/spree/olitt/clone_store/product_helpers_spec.rb +++ b/spec/helpers/spree/olitt/clone_store/product_helpers_spec.rb @@ -55,6 +55,27 @@ module CloneStore expect(new_variant).to have_received(:option_values=).with(option_values) end end + + describe '#assign_variant_prices' do + it 'does not assign a blank fallback price when the source variant has no prices or price amount' do + old_variant = instance_double('Spree::Variant', prices: [], price: nil) + new_variant = instance_double('Spree::Variant') + + allow(new_variant).to receive(:respond_to?).with(:prices=).and_return(true) + allow(new_variant).to receive(:respond_to?).with(:price=).and_return(true) + allow(new_variant).to receive(:respond_to?).with(:currency=).and_return(true) + allow(old_variant).to receive(:respond_to?).with(:price).and_return(true) + allow(old_variant).to receive(:respond_to?).with(:currency).and_return(true) + allow(new_variant).to receive(:prices=) + allow(new_variant).to receive(:price=) + allow(new_variant).to receive(:currency=) + + helper_host.assign_variant_prices(new_variant: new_variant, old_variant: old_variant) + + expect(new_variant).not_to have_received(:price=) + expect(new_variant).not_to have_received(:currency=) + end + end end end end diff --git a/spec/services/spree/olitt/clone_store/clone_request_presenter_spec.rb b/spec/services/spree/olitt/clone_store/clone_request_presenter_spec.rb index c72e897..9b4be4c 100644 --- a/spec/services/spree/olitt/clone_store/clone_request_presenter_spec.rb +++ b/spec/services/spree/olitt/clone_store/clone_request_presenter_spec.rb @@ -15,6 +15,10 @@ started_at: nil, finished_at: nil, error_message: nil, + vendor_id: 11, + vendor_email: 'vendor@example.com', + vendor: instance_double(Spree::Vendor, slug: 'vendor-slug'), + admin_user: instance_double(Spree.admin_user_class, id: 77), store: nil, fallback_store_payload: { data: { id: '10', type: 'store', attributes: { name: 'Clone' } } } ) @@ -30,7 +34,14 @@ status: 'queued', source_store_id: 9, queue_name: 'default', - queued_at: '2026-04-21T07:28:00.952Z' + queued_at: '2026-04-21T07:28:00.952Z', + vendor: { + vendor_id: 11, + vendor_slug: 'vendor-slug', + admin_user_id: 77, + email: 'vendor@example.com', + next_path: '/admin' + } }) end end diff --git a/spec/services/spree/olitt/clone_store/clone_request_provisioner_spec.rb b/spec/services/spree/olitt/clone_store/clone_request_provisioner_spec.rb new file mode 100644 index 0000000..263b6c1 --- /dev/null +++ b/spec/services/spree/olitt/clone_store/clone_request_provisioner_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Spree::Olitt::CloneStore::CloneRequestProvisioner do + describe '#call' do + it 'provisions an admin user and links it to the vendor' do + source_store = create(:store, default: false, url: 'source.example.com', code: 'source-store') + clone_request = Spree::Olitt::CloneStore::CloneRequest.create!( + source_store: source_store, + store_name: 'Clone Store', + store_url: 'clone.example.com', + store_code: 'clone-store', + store_mail_from_address: 'clone@example.com', + vendor_email: 'vendor.clone@example.com', + vendor_password: 'Password123!' + ) + + provisioner = described_class.new(clone_request: clone_request) + + expect(provisioner.call).to be(true) + + clone_request.reload + expect(clone_request.admin_user).to be_present + expect(clone_request.admin_user).to be_a(Spree.admin_user_class) + expect(clone_request.admin_user.email).to eq('vendor.clone@example.com') + expect(clone_request.admin_user.valid_password?('Password123!')).to be(true) + expect(clone_request.vendor).to be_present + expect(Spree::RoleUser.exists?(user: clone_request.admin_user, resource: clone_request.vendor)).to be(true) + expect(Spree::VendorUser.exists?(vendor: clone_request.vendor, admin_user: clone_request.admin_user)).to be(true) + expect(clone_request.store.vendor_id).to eq(clone_request.vendor.id) + end + end +end \ No newline at end of file diff --git a/spec/services/spree/olitt/clone_store/duplicators/shipping_categories_duplicator_spec.rb b/spec/services/spree/olitt/clone_store/duplicators/shipping_categories_duplicator_spec.rb index ba55cca..67a84cc 100644 --- a/spec/services/spree/olitt/clone_store/duplicators/shipping_categories_duplicator_spec.rb +++ b/spec/services/spree/olitt/clone_store/duplicators/shipping_categories_duplicator_spec.rb @@ -11,14 +11,20 @@ module Duplicators product = instance_double('Spree::Product', shipping_category: shipping_category) products_relation = instance_double('ActiveRecord::Relation') source_vendor = instance_double('Spree::Vendor', shipping_methods: []) - old_store = instance_double('Spree::Store', products: products_relation, vendor: source_vendor) + old_store = double('Spree::Store', products: products_relation, vendor: source_vendor) vendor = instance_double('Spree::Vendor', id: 19) new_shipping_category = instance_double('Spree::ShippingCategory') vendor_shipping_categories = instance_double('ActiveRecord::Relation') + vendor_shipping_categories_without_current = instance_double('ActiveRecord::Relation') + shipping_categories_named_scope = instance_double('ActiveRecord::Relation') allow(products_relation).to receive(:includes).with(:shipping_category).and_return([product]) allow(Spree::ShippingCategory).to receive(:where).with(vendor_id: 19).and_return(vendor_shipping_categories) allow(vendor_shipping_categories).to receive(:find_by).with(name: 'Physical Goods').and_return(nil) + allow(vendor_shipping_categories).to receive(:where).and_return(vendor_shipping_categories_without_current) + allow(vendor_shipping_categories_without_current).to receive(:not).and_return(vendor_shipping_categories_without_current) + allow(vendor_shipping_categories_without_current).to receive(:where).with(name: 'Physical Goods').and_return(shipping_categories_named_scope) + allow(shipping_categories_named_scope).to receive(:exists?).and_return(false) allow(Spree::ShippingCategory).to receive(:new).and_return(new_shipping_category) allow(new_shipping_category).to receive(:vendor_id=) diff --git a/spec/services/spree/olitt/clone_store/duplicators/stock_locations_duplicator_spec.rb b/spec/services/spree/olitt/clone_store/duplicators/stock_locations_duplicator_spec.rb index c3c18d7..b5cc768 100644 --- a/spec/services/spree/olitt/clone_store/duplicators/stock_locations_duplicator_spec.rb +++ b/spec/services/spree/olitt/clone_store/duplicators/stock_locations_duplicator_spec.rb @@ -29,18 +29,22 @@ module Duplicators stock_locations_relation = instance_double('ActiveRecord::Relation') source_vendor = instance_double('Spree::Vendor', stock_locations: stock_locations_relation) - old_store = instance_double('Spree::Store', vendor: source_vendor) + old_store = double('Spree::Store', vendor: source_vendor) new_store = instance_double('Spree::Store', name: 'New Store', default_country: :country) vendor_stock_locations = instance_double('ActiveRecord::Relation') vendor = instance_double('Spree::Vendor', id: 9, name: 'vendor@example.com', stock_locations: vendor_stock_locations) new_location = instance_double('Spree::StockLocation') + vendor_stock_locations_without_current = instance_double('ActiveRecord::Relation') + stock_locations_named_scope = instance_double('ActiveRecord::Relation') allow(stock_locations_relation).to receive(:includes).with(:country, :state).and_return(stock_locations_relation) allow(stock_locations_relation).to receive(:order).with(default: :desc, id: :asc).and_return([old_location]) allow(vendor_stock_locations).to receive(:order).with(default: :desc, id: :asc).and_return([]) allow(vendor_stock_locations).to receive(:find_by).with(name: 'Main Warehouse').and_return(nil) - allow(vendor_stock_locations).to receive(:where).and_return(vendor_stock_locations) - allow(vendor_stock_locations).to receive(:exists?).and_return(false) + allow(vendor_stock_locations).to receive(:where).and_return(vendor_stock_locations_without_current) + allow(vendor_stock_locations_without_current).to receive(:not).and_return(vendor_stock_locations_without_current) + allow(vendor_stock_locations_without_current).to receive(:where).with(name: 'Main Warehouse').and_return(stock_locations_named_scope) + allow(stock_locations_named_scope).to receive(:exists?).and_return(false) allow(Spree::StockLocation).to receive(:new).and_return(new_location) allow(new_location).to receive(:name=) @@ -75,17 +79,21 @@ module Duplicators it 'creates a fallback default stock location when the source store has none' do products_relation = instance_double('ActiveRecord::Relation') - old_store = instance_double('Spree::Store', vendor: nil, products: products_relation) + old_store = double('Spree::Store', vendor: nil, products: products_relation) new_store = instance_double('Spree::Store', name: 'Gallery shop', default_country: :us_country) vendor_stock_locations = instance_double('ActiveRecord::Relation') vendor = instance_double('Spree::Vendor', id: 9, name: 'vendor@example.com', stock_locations: vendor_stock_locations) new_location = instance_double('Spree::StockLocation') + vendor_stock_locations_without_current = instance_double('ActiveRecord::Relation') + stock_locations_named_scope = instance_double('ActiveRecord::Relation') allow(products_relation).to receive(:includes).with(variants_including_master: { stock_items: :stock_location }).and_return([]) allow(vendor_stock_locations).to receive(:order).with(default: :desc, id: :asc).and_return([]) allow(vendor_stock_locations).to receive(:find_by).with(default: true).and_return(nil) - allow(vendor_stock_locations).to receive(:where).and_return(vendor_stock_locations) - allow(vendor_stock_locations).to receive(:exists?).and_return(false) + allow(vendor_stock_locations).to receive(:where).and_return(vendor_stock_locations_without_current) + allow(vendor_stock_locations_without_current).to receive(:not).and_return(vendor_stock_locations_without_current) + allow(vendor_stock_locations_without_current).to receive(:where).with(name: 'US location').and_return(stock_locations_named_scope) + allow(stock_locations_named_scope).to receive(:exists?).and_return(false) allow(Spree::StockLocation).to receive(:new).and_return(new_location) allow(new_location).to receive(:name=)