Skip to content

Commit

Permalink
Add a configurable order number generator
Browse files Browse the repository at this point in the history
The default order number generator was extracted into a class.

Provide your own class instance that takes an options hash and returns a string when called for `#generate`.

    Spree::Config.order_number_generator = TrueNumberGenerator.new

    class TrueNumberGenerator
      def initialize(options = {})
      end

      def generate
        '42'
      end
    end
  • Loading branch information
tvdeyen committed Jun 7, 2017
1 parent 567fa49 commit 92622c5
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 40 deletions.
11 changes: 11 additions & 0 deletions core/app/models/spree/app_configuration.rb
Expand Up @@ -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
Expand Down
27 changes: 9 additions & 18 deletions 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.
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions 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
45 changes: 45 additions & 0 deletions 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
55 changes: 33 additions & 22 deletions core/spec/models/spree/order_spec.rb
Expand Up @@ -690,41 +690,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
Expand Down

0 comments on commit 92622c5

Please sign in to comment.