diff --git a/core/app/models/spree/app_configuration.rb b/core/app/models/spree/app_configuration.rb index c5978762f36..3277a980570 100644 --- a/core/app/models/spree/app_configuration.rb +++ b/core/app/models/spree/app_configuration.rb @@ -397,6 +397,17 @@ def current_store_selector_class @current_store_selector_class ||= Spree::CurrentStoreSelector end + # Allows providing your own class instance for generating order numbers. + # + # @!attribute [rw] order_number_generator + # @return [Class] a class instance with the same public interfaces as + # Spree::Order::NumberGenerator + # @api experimental + attr_writer :order_number_generator + def order_number_generator + @order_number_generator ||= Spree::Order::NumberGenerator.new + end + def static_model_preferences @static_model_preferences ||= Spree::Preferences::StaticModelPreferences.new end diff --git a/core/app/models/spree/order.rb b/core/app/models/spree/order.rb index 5acd85d6008..4fd8fdd062a 100644 --- a/core/app/models/spree/order.rb +++ b/core/app/models/spree/order.rb @@ -1,5 +1,6 @@ require 'spree/core/validators/email' require 'spree/order/checkout' +require 'spree/order/number_generator' module Spree # The customers cart until completed, then acts as permanent record of the transaction. @@ -298,25 +299,15 @@ def associate_user!(user, override_email = true) assign_attributes(attrs_to_set) end - def generate_order_number(options = {}) - options[:length] ||= ORDER_NUMBER_LENGTH - options[:letters] ||= ORDER_NUMBER_LETTERS - options[:prefix] ||= ORDER_NUMBER_PREFIX - - possible = (0..9).to_a - possible += ('A'..'Z').to_a if options[:letters] - - self.number ||= loop do - # Make a random number. - random = "#{options[:prefix]}#{(0...options[:length]).map { possible.sample }.join}" - # Use the random number if no other order exists with it. - if self.class.exists?(number: random) - # If over half of all possible options are taken add another digit. - options[:length] += 1 if self.class.count > (10**options[:length] / 2) - else - break random - end + def generate_order_number(options = nil) + if options + Spree::Deprecation.warn \ + "Passing options to Order#generate_order_number is deprecated. " \ + "Please add your own instance of the order number generator " \ + "with your options (#{options.inspect}) and store it as " \ + "Spree::Config.order_number_generator in your stores config." end + self.number ||= Spree::Config.order_number_generator.generate end def shipped_shipments diff --git a/core/app/models/spree/order/number_generator.rb b/core/app/models/spree/order/number_generator.rb new file mode 100644 index 00000000000..2df846ea6b1 --- /dev/null +++ b/core/app/models/spree/order/number_generator.rb @@ -0,0 +1,43 @@ +module Spree + # Generates order numbers + # + # In order to change the way your order numbers get generated you can either + # set your own instance of this class in your stores configuration with different options: + # + # Spree::Config.order_number_generator = Spree::Order::NumberGenerator.new( + # prefix: 'B', + # lenght: 8, + # letters: false + # ) + # + # or create your own class: + # + # Spree::Config.order_number_generator = My::OrderNumberGenerator.new + # + class Order::NumberGenerator + attr_reader :letters, :prefix + + def initialize(options = {}) + @length = options[:length] || Spree::Order::ORDER_NUMBER_LENGTH + @letters = options[:letters] || Spree::Order::ORDER_NUMBER_LETTERS + @prefix = options[:prefix] || Spree::Order::ORDER_NUMBER_PREFIX + end + + def generate + possible = (0..9).to_a + possible += ('A'..'Z').to_a if letters + + loop do + # Make a random number. + random = "#{prefix}#{(0...@length).map { possible.sample }.join}" + # Use the random number if no other order exists with it. + if Spree::Order.exists?(number: random) + # If over half of all possible options are taken add another digit. + @length += 1 if Spree::Order.count > (10**@length / 2) + else + break random + end + end + end + end +end diff --git a/core/spec/models/spree/order/number_generator_spec.rb b/core/spec/models/spree/order/number_generator_spec.rb new file mode 100644 index 00000000000..61d652120c0 --- /dev/null +++ b/core/spec/models/spree/order/number_generator_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +RSpec.describe Spree::Order::NumberGenerator do + subject { described_class.new.generate } + + it { is_expected.to be_a(String) } + + describe 'length' do + let(:default_length) do + Spree::Order::ORDER_NUMBER_LENGTH + Spree::Order::ORDER_NUMBER_PREFIX.length + end + + it { expect(subject.length).to eq default_length } + + context "when length option is 5" do + let(:option_length) { 5 + Spree::Order::ORDER_NUMBER_PREFIX.length } + + subject { described_class.new(length: 5).generate } + + it "should be 5 plus default prefix length" do + expect(subject.length).to eq option_length + end + end + end + + context "when letters option is true" do + subject { described_class.new(letters: true).generate } + + it "generates order number including letters" do + is_expected.to match /[A-Z]/ + end + end + + describe 'prefix' do + it { is_expected.to match /^#{Spree::Order::ORDER_NUMBER_PREFIX}/ } + + context "when prefix option is 'P'" do + subject { described_class.new(prefix: 'P').generate } + + it "generates order number prefixed with 'P'" do + is_expected.to match /^P/ + end + end + end +end diff --git a/core/spec/models/spree/order_spec.rb b/core/spec/models/spree/order_spec.rb index 08ea4e207bc..982420f1a82 100644 --- a/core/spec/models/spree/order_spec.rb +++ b/core/spec/models/spree/order_spec.rb @@ -705,41 +705,52 @@ def merge!(other_order, user = nil) end end - context "#generate_order_number" do + describe "#generate_order_number" do let(:order) { build(:order) } - context "when no configure" do - let(:default_length) { Spree::Order::ORDER_NUMBER_LENGTH + Spree::Order::ORDER_NUMBER_PREFIX.length } - subject(:order_number) { order.generate_order_number } + context "with default app configuration" do + it 'calls the default order number generator' do + expect_any_instance_of(Spree::Order::NumberGenerator).to receive(:generate) + order.generate_order_number + end + end + + context "with order number generator configured" do + class TruthNumberGenerator + def initialize(options = {}); end + + def generate + '42' + end + end - describe '#class' do - subject { super().class } - it { is_expected.to eq String } + before do + expect(Spree::Config).to receive(:order_number_generator) do + TruthNumberGenerator.new + end end - describe '#length' do - subject { super().length } - it { is_expected.to eq default_length } + it 'calls the configured order number generator' do + order.generate_order_number + expect(order.number).to eq '42' end - it { is_expected.to match /^#{Spree::Order::ORDER_NUMBER_PREFIX}/ } end - context "when length option is 5" do - let(:option_length) { 5 + Spree::Order::ORDER_NUMBER_PREFIX.length } - it "should be option length for order number" do - expect(order.generate_order_number(length: 5).length).to eq option_length + context "with number already present" do + before do + order.number = '123' end - end - context "when letters option is true" do - it "generates order number include letter" do - expect(order.generate_order_number(length: 100, letters: true)).to match /[A-Z]/ + it 'does not generate new number' do + order.generate_order_number + expect(order.number).to eq '123' end end - context "when prefix option is 'P'" do - it "generates order number and it prefix is 'P'" do - expect(order.generate_order_number(prefix: 'P')).to match /^P/ + context "passing options" do + it 'is deprecated' do + expect(Spree::Deprecation).to receive(:warn) + order.generate_order_number(length: 2) end end end