solidus_reserved_stock 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +26 -0
- data/app/controllers/spree/admin/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/admin/stock_items_controller_decorator.rb +7 -0
- data/app/controllers/spree/admin/users_controller_decorator.rb +16 -0
- data/app/controllers/spree/api/products_controller_decorator.rb +12 -0
- data/app/controllers/spree/api/v1/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/api/v1/validators/reserve_stock_params_validator.rb +37 -0
- data/app/controllers/spree/api/variants_controller_decorator.rb +12 -0
- data/app/helpers/spree/admin/reserved_stock_items_helper.rb +13 -0
- data/app/helpers/spree/admin/stock_locations_helper_decorator.rb +14 -0
- data/app/helpers/spree/api/api_helpers_decorator.rb +7 -0
- data/app/models/spree/product_decorator.rb +18 -0
- data/app/models/spree/reserved_stock_item.rb +48 -0
- data/app/models/spree/stock/coordinator_decorator.rb +28 -0
- data/app/models/spree/stock/prioritizer_decorator.rb +16 -0
- data/app/models/spree/stock/quantifier_decorator.rb +29 -0
- data/app/models/spree/stock/reserver.rb +60 -0
- data/app/models/spree/stock_location_decorator.rb +46 -0
- data/app/models/spree/stock_reservation_ability.rb +11 -0
- data/app/models/spree/user_decorator.rb +42 -0
- data/app/models/spree/variant_decorator.rb +6 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/adjust_number_of_rows.html.erb.deface +2 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/show_user_for_reserved_stock.html.erb.deface +2 -0
- data/app/overrides/spree/admin/users/_sidebar/add_reserved_stock_menu_item.html.erb.deface +6 -0
- data/app/views/spree/admin/reserved_stock_items/_form.html.erb +32 -0
- data/app/views/spree/admin/reserved_stock_items/index.html.erb +104 -0
- data/app/views/spree/admin/reserved_stock_items/new.html.erb +19 -0
- data/app/views/spree/api/products/show.v1.rabl +35 -0
- data/app/views/spree/api/v1/reserved_stock_items/index.v1.rabl +7 -0
- data/app/views/spree/api/v1/reserved_stock_items/show.v1.rabl +5 -0
- data/app/views/spree/api/variants/big.v1.rabl +20 -0
- data/app/views/spree/api/variants/small.v1.rabl +17 -0
- data/bin/console +14 -0
- data/bin/rails +12 -0
- data/bin/setup +7 -0
- data/config/i18n-tasks.yml +103 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20160105203812_add_reserved_items_to_stock_location.rb +6 -0
- data/db/migrate/20160105222821_add_type_to_spree_stock_items.rb +7 -0
- data/db/migrate/20160106215753_add_user_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160229223744_add_original_stock_location_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160301003702_add_expires_at_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160309025334_modify_stock_item_unique_index.rb +14 -0
- data/lib/generators/solidus_reserved_stock/install/install_generator.rb +22 -0
- data/lib/solidus_reserved_stock/ability_initializer.rb +5 -0
- data/lib/solidus_reserved_stock/engine.rb +15 -0
- data/lib/solidus_reserved_stock/version.rb +18 -0
- data/lib/solidus_reserved_stock.rb +7 -0
- data/lib/tasks/solidus_reserved_stock_tasks.rake +4 -0
- data/solidus_reserved_stock.gemspec +56 -0
- data/spec/controllers/spree/api/stock_locations_controller_spec.rb +24 -0
- data/spec/controllers/spree/api/v1/reserved_stock_items_controller_spec.rb +196 -0
- data/spec/controllers/spree/api/v1/validators/reserve_stock_params_validator_spec.rb +41 -0
- data/spec/controllers/spree/api/variants_controller_spec.rb +62 -0
- data/spec/factories/reserved_stock_item_factory.rb +8 -0
- data/spec/models/spree/reserved_stock_item_spec.rb +101 -0
- data/spec/models/spree/stock/coordinator_decorator_spec.rb +68 -0
- data/spec/models/spree/stock/prioritizer_decorator_spec.rb +29 -0
- data/spec/models/spree/stock/quantifier_decorator_spec.rb +42 -0
- data/spec/models/spree/stock/reserver_spec.rb +103 -0
- data/spec/models/spree/stock_location_decorator_spec.rb +47 -0
- data/spec/models/spree/user_decorator_spec.rb +65 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/api_helpers.rb +35 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/have_attributes_matcher.rb +10 -0
- metadata +396 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# We need to be able to reserve the same variant for more than one user.
|
2
|
+
# This relaxes the uniqueness constraint introduced in Solidus 1.2 for
|
3
|
+
# supporting databases.
|
4
|
+
class ModifyStockItemUniqueIndex < ActiveRecord::Migration
|
5
|
+
def change
|
6
|
+
return unless connection.adapter_name =~ /postgres|sqlite/i
|
7
|
+
remove_index "spree_stock_items", ["variant_id", "stock_location_id"]
|
8
|
+
add_index "spree_stock_items",
|
9
|
+
["variant_id", "stock_location_id", "user_id"],
|
10
|
+
where: "deleted_at is null",
|
11
|
+
unique: true,
|
12
|
+
name: "index_spree_stock_items_on_variant_stock_location_and_user"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SolidusReservedStock
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
class_option :auto_run_migrations, type: :boolean, default: true
|
5
|
+
|
6
|
+
def add_migrations
|
7
|
+
run "bin/rake solidus_reserved_stock:install:migrations"
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_migrations
|
11
|
+
if options[:auto_run_migrations] ||
|
12
|
+
["", "y", "Y"].include?(
|
13
|
+
ask("Would you like to run the migrations now? [Y/n]")
|
14
|
+
)
|
15
|
+
run "bin/rake db:migrate"
|
16
|
+
else
|
17
|
+
puts "Skiping rake db:migrate, don't forget to run it!"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SolidusReservedStock
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
engine_name "solidus_reserved_stock"
|
4
|
+
config.autoload_paths += %W(#{config.root}/lib)
|
5
|
+
|
6
|
+
def self.activate
|
7
|
+
Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator*.rb")) do |c|
|
8
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
config.to_prepare(&method(:activate).to_proc)
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SolidusReservedStock
|
2
|
+
module_function
|
3
|
+
|
4
|
+
# Returns the version of the currently loaded SolidusReservedStock as a
|
5
|
+
# <tt>Gem::Version</tt>.
|
6
|
+
def version
|
7
|
+
Gem::Version.new VERSION::STRING
|
8
|
+
end
|
9
|
+
|
10
|
+
module VERSION
|
11
|
+
MAJOR = 0
|
12
|
+
MINOR = 0
|
13
|
+
TINY = 1
|
14
|
+
PRE = nil
|
15
|
+
|
16
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib/", __FILE__)
|
3
|
+
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require "solidus_reserved_stock/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.name = "solidus_reserved_stock"
|
10
|
+
s.version = SolidusReservedStock.version
|
11
|
+
s.homepage = "https://github.com/resolve/solidus_reserved_stock"
|
12
|
+
s.license = "MIT"
|
13
|
+
s.summary = <<-HEREDOC
|
14
|
+
Allows stock to be reserved for a given user, so it can't be purchased by other
|
15
|
+
users.
|
16
|
+
HEREDOC
|
17
|
+
s.description = <<-HEREDOC
|
18
|
+
Allow stock to be reserved for a given user, so it can't be purchased by other
|
19
|
+
users.
|
20
|
+
When a customer reserves stock, it's moved from its normal stock location to a
|
21
|
+
special reserved stock location. When a customer checks out, their reserved
|
22
|
+
items will be used first to fulfill their order.
|
23
|
+
Reserved stock can be restored to its original stock location at any time, and
|
24
|
+
can be stored with an expiry date for the reservation.
|
25
|
+
HEREDOC
|
26
|
+
|
27
|
+
s.author = "Isaac Freeman"
|
28
|
+
s.email = "isaac@resolvedigital.co.nz"
|
29
|
+
|
30
|
+
s.files = `git ls-files`.split("\n")
|
31
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
32
|
+
s.require_path = "lib"
|
33
|
+
s.requirements << "none"
|
34
|
+
|
35
|
+
s.has_rdoc = false
|
36
|
+
|
37
|
+
s.add_runtime_dependency "solidus_core", "~> 1.2"
|
38
|
+
s.add_runtime_dependency "solidus_api", "~> 1.2"
|
39
|
+
s.add_runtime_dependency "solidus_backend", "~> 1.2"
|
40
|
+
s.add_runtime_dependency "deface", "~> 1.0"
|
41
|
+
|
42
|
+
s.add_development_dependency "byebug", "~> 8.2"
|
43
|
+
s.add_development_dependency "capybara", "~> 2.4"
|
44
|
+
s.add_development_dependency "coffee-rails", "~> 4.0"
|
45
|
+
s.add_development_dependency "database_cleaner", "~> 1.3"
|
46
|
+
s.add_development_dependency "factory_girl_rails", "~> 4.6"
|
47
|
+
s.add_development_dependency "ffaker", "~> 1.32"
|
48
|
+
s.add_development_dependency "poltergeist", "~> 1.5"
|
49
|
+
s.add_development_dependency "pry-rails", "~> 0.3"
|
50
|
+
s.add_development_dependency "rubocop", "~> 0.37"
|
51
|
+
s.add_development_dependency "rspec-rails", "~> 3.1"
|
52
|
+
s.add_development_dependency "rspec-activemodel-mocks", "~> 1.0"
|
53
|
+
s.add_development_dependency "sass-rails", "~> 5.0"
|
54
|
+
s.add_development_dependency "simplecov", "~> 0.9"
|
55
|
+
s.add_development_dependency "guard-rspec", "~> 4.6"
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
describe Api::StockLocationsController, type: :controller do
|
5
|
+
render_views
|
6
|
+
|
7
|
+
let!(:stock_location) { create(:stock_location) }
|
8
|
+
|
9
|
+
before do
|
10
|
+
stub_authentication!
|
11
|
+
end
|
12
|
+
|
13
|
+
context "as an admin" do
|
14
|
+
sign_in_as_admin!
|
15
|
+
|
16
|
+
describe "#index" do
|
17
|
+
it "includes the reserved_items attribute" do
|
18
|
+
api_get :index
|
19
|
+
expect(json_response["stock_locations"].first).to have_attributes([:reserved_items])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
describe Api::V1::ReservedStockItemsController, type: :controller do
|
5
|
+
render_views
|
6
|
+
|
7
|
+
let(:user) { create(:user) }
|
8
|
+
let(:original_stock_location) { create(:stock_location) }
|
9
|
+
let(:variant) { create(:variant) }
|
10
|
+
let(:reserved_stock_location) { Spree::StockLocation.reserved_items_location }
|
11
|
+
let(:reserved_stock_item) do
|
12
|
+
create(
|
13
|
+
:reserved_stock_item,
|
14
|
+
variant: variant,
|
15
|
+
stock_location: reserved_stock_location,
|
16
|
+
original_stock_location: original_stock_location,
|
17
|
+
user: user,
|
18
|
+
expires_at: 1.day.from_now,
|
19
|
+
backorderable: false
|
20
|
+
)
|
21
|
+
end
|
22
|
+
let(:expired_reserved_stock_item) do
|
23
|
+
create(
|
24
|
+
:reserved_stock_item,
|
25
|
+
variant: variant,
|
26
|
+
stock_location: reserved_stock_location,
|
27
|
+
original_stock_location: original_stock_location,
|
28
|
+
user: user,
|
29
|
+
expires_at: 1.day.ago,
|
30
|
+
backorderable: false
|
31
|
+
)
|
32
|
+
end
|
33
|
+
let(:reserved_stock_item_attributes) {
|
34
|
+
[
|
35
|
+
:id,
|
36
|
+
:count_on_hand,
|
37
|
+
:expires_at,
|
38
|
+
:original_stock_location_id,
|
39
|
+
:stock_location_id,
|
40
|
+
:user_id,
|
41
|
+
:variant_id,
|
42
|
+
:variant
|
43
|
+
]
|
44
|
+
}
|
45
|
+
|
46
|
+
before do
|
47
|
+
stub_authentication!
|
48
|
+
end
|
49
|
+
|
50
|
+
context "as a normal user" do
|
51
|
+
it "cannot list reserved items" do
|
52
|
+
api_get :index
|
53
|
+
assert_unauthorized!
|
54
|
+
end
|
55
|
+
it "cannot reserve stock" do
|
56
|
+
api_post :reserve,
|
57
|
+
variant: variant, original_stock_location: original_stock_location, user: user, quantity: 4
|
58
|
+
assert_unauthorized!
|
59
|
+
end
|
60
|
+
it "cannot restock" do
|
61
|
+
api_post :restock, variant: variant, user: user, quantity: 4
|
62
|
+
assert_unauthorized!
|
63
|
+
end
|
64
|
+
it "cannot restock expired" do
|
65
|
+
api_post :restock_expired
|
66
|
+
assert_unauthorized!
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "as an admin" do
|
71
|
+
sign_in_as_admin!
|
72
|
+
|
73
|
+
describe "#index" do
|
74
|
+
it "can list reserved stock items" do
|
75
|
+
reserved_stock_item
|
76
|
+
api_get :index
|
77
|
+
expect(response).to be_success
|
78
|
+
expect(json_response['reserved_stock_items'].first).to have_attributes(reserved_stock_item_attributes)
|
79
|
+
expect(json_response['reserved_stock_items'].first['variant']['sku']).to match variant.sku
|
80
|
+
end
|
81
|
+
it "can list reserved stock items for a given user" do
|
82
|
+
reserved_stock_item
|
83
|
+
api_get :index, user_id: user.id
|
84
|
+
expect(response).to be_success
|
85
|
+
expect(json_response['reserved_stock_items'].first).to have_attributes(reserved_stock_item_attributes)
|
86
|
+
expect(json_response['reserved_stock_items'].first['variant']['sku']).to match variant.sku
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#reserve" do
|
91
|
+
context "request that will succeed" do
|
92
|
+
before do
|
93
|
+
stock_item = original_stock_location.stock_item_or_create(variant)
|
94
|
+
stock_item.adjust_count_on_hand(10)
|
95
|
+
end
|
96
|
+
it "reserves a quantity of stock for a given variant" do
|
97
|
+
expiry_date = 1.week.from_now
|
98
|
+
api_post :reserve,
|
99
|
+
variant_id: variant.id,
|
100
|
+
original_stock_location_id: original_stock_location.id,
|
101
|
+
user_id: user.id,
|
102
|
+
quantity: 4,
|
103
|
+
expires_at: expiry_date
|
104
|
+
expect(response.status).to eq 201
|
105
|
+
expect(json_response[:count_on_hand]).to eq 4
|
106
|
+
expect(json_response[:expires_at]).to eq expiry_date.iso8601(3)
|
107
|
+
end
|
108
|
+
it "can find variants by sku" do
|
109
|
+
api_post :reserve,
|
110
|
+
sku: variant.sku,
|
111
|
+
original_stock_location_id: original_stock_location.id,
|
112
|
+
user_id: user.id,
|
113
|
+
quantity: 4
|
114
|
+
expect(response.status).to eq 201
|
115
|
+
end
|
116
|
+
end
|
117
|
+
context "can't find variant" do
|
118
|
+
it "returns an approprate error" do
|
119
|
+
api_post :reserve,
|
120
|
+
sku: "florb",
|
121
|
+
original_stock_location_id: original_stock_location.id,
|
122
|
+
user_id: user.id,
|
123
|
+
quantity: 4
|
124
|
+
expect(response.status).to eq 422
|
125
|
+
end
|
126
|
+
end
|
127
|
+
context "can't find original stock location" do
|
128
|
+
it "returns an approprate error" do
|
129
|
+
api_post :reserve,
|
130
|
+
sku: variant.sku,
|
131
|
+
original_stock_location_id: 1000,
|
132
|
+
user_id: user.id,
|
133
|
+
quantity: 4
|
134
|
+
expect(response.status).to eq 422
|
135
|
+
end
|
136
|
+
end
|
137
|
+
context "can't find user" do
|
138
|
+
it "returns an approprate error" do
|
139
|
+
api_post :reserve,
|
140
|
+
sku: variant.sku,
|
141
|
+
original_stock_location_id: original_stock_location.id,
|
142
|
+
user_id: 1000,
|
143
|
+
quantity: 4
|
144
|
+
expect(response.status).to eq 422
|
145
|
+
end
|
146
|
+
end
|
147
|
+
context "quantity unavailable" do
|
148
|
+
it "returns an approprate error" do
|
149
|
+
api_post :reserve,
|
150
|
+
sku: variant.sku,
|
151
|
+
original_stock_location_id: original_stock_location.id,
|
152
|
+
user_id: user.id,
|
153
|
+
quantity: 10000
|
154
|
+
expect(response.status).to eq 422
|
155
|
+
end
|
156
|
+
end
|
157
|
+
context "parameters are missing" do
|
158
|
+
it "returns JSON API formatted errors" do
|
159
|
+
api_post :reserve
|
160
|
+
expect(response.status).to eq 422
|
161
|
+
body = JSON.parse response.body
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "#restock" do
|
167
|
+
before :each do
|
168
|
+
reserved_stock_item
|
169
|
+
end
|
170
|
+
it "restores all stock if no quantity given" do
|
171
|
+
api_post :restock, variant_id: variant.id, user_id: user.id
|
172
|
+
expect(response.status).to eq 201
|
173
|
+
expect(json_response[:count_on_hand]).to eq 0
|
174
|
+
end
|
175
|
+
it "restores some stock if quantity given" do
|
176
|
+
api_post :restock, variant_id: variant.id, user_id: user.id, quantity: 4
|
177
|
+
expect(response.status).to eq 201
|
178
|
+
expect(json_response[:count_on_hand]).to eq 6
|
179
|
+
end
|
180
|
+
it "returns an appropriate error message on failure" do
|
181
|
+
api_post :restock, variant: "flarn"
|
182
|
+
expect(response.status).to eq 422
|
183
|
+
# TODO: test for a reasonable error message
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#restock_expired" do
|
188
|
+
it "restores expired reservations" do
|
189
|
+
expired_reserved_stock_item
|
190
|
+
api_post :restock_expired
|
191
|
+
expect(response.status).to eq 204
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
describe Spree::Api::V1::Validators::ReserveStockParamsValidator do
|
5
|
+
subject { Spree::Api::V1::Validators::ReserveStockParamsValidator }
|
6
|
+
let(:valid_parameters) {
|
7
|
+
{
|
8
|
+
variant_id: 1,
|
9
|
+
original_stock_location_id: 1,
|
10
|
+
user_id: 1,
|
11
|
+
quantity: 1,
|
12
|
+
expires_at: 1.day.from_now
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
it "fails validation when there's no variant_id or sku parameter" do
|
17
|
+
validator = subject.new(valid_parameters.except :variant_id)
|
18
|
+
expect(validator.validate).to be false
|
19
|
+
expect(validator.errors).to eq [{ status: 422, detail: "Parameters must include either 'variant_id' or 'sku'"}]
|
20
|
+
end
|
21
|
+
|
22
|
+
it "fails validation when there's no original_stock_location_id parameter" do
|
23
|
+
validator = subject.new(valid_parameters.except :original_stock_location_id)
|
24
|
+
expect(validator.validate).to be false
|
25
|
+
expect(validator.errors).to eq [{ status: 422, detail: "Parameters must include 'original_stock_location_id'"}]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "fails validation when there's no user_id parameter" do
|
29
|
+
validator = subject.new(valid_parameters.except :user_id)
|
30
|
+
expect(validator.validate).to be false
|
31
|
+
expect(validator.errors).to eq [{ status: 422, detail: "Parameters must include 'user_id'"}]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "fails validation when there's no quantity parameter" do
|
35
|
+
validator = subject.new(valid_parameters.except :quantity)
|
36
|
+
expect(validator.validate).to be false
|
37
|
+
expect(validator.errors).to eq [{ status: 422, detail: "Parameters must include 'quantity'"}]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
describe Api::VariantsController, type: :controller do
|
5
|
+
render_views
|
6
|
+
|
7
|
+
let(:variant) { create(:variant) }
|
8
|
+
let(:user) { create(:user) }
|
9
|
+
let(:other_user) { create(:user) }
|
10
|
+
let(:original_stock_location) { create(:stock_location) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
stub_authentication!
|
14
|
+
stock_item = original_stock_location.stock_item_or_create(variant)
|
15
|
+
stock_item.adjust_count_on_hand(10)
|
16
|
+
Spree::Stock::Reserver.new.reserve(
|
17
|
+
variant,
|
18
|
+
original_stock_location,
|
19
|
+
user,
|
20
|
+
4
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "as an admin" do
|
25
|
+
sign_in_as_admin!
|
26
|
+
|
27
|
+
describe "#show" do
|
28
|
+
context "total_on_hand" do
|
29
|
+
context "when given user" do
|
30
|
+
it "returns total that includes reserved stock" do
|
31
|
+
api_get :show, id: variant.id, user_id: user.id
|
32
|
+
expect(json_response[:total_on_hand]).to eq 10
|
33
|
+
end
|
34
|
+
end
|
35
|
+
context "when given no user" do
|
36
|
+
it "returns total that includes reserved stock" do
|
37
|
+
api_get :show, id: variant.id
|
38
|
+
expect(json_response[:total_on_hand]).to eq 6
|
39
|
+
end
|
40
|
+
end
|
41
|
+
context "when given a different user" do
|
42
|
+
it "returns total that includes reserved stock" do
|
43
|
+
api_get :show, id: variant.id, user_id: other_user.id
|
44
|
+
expect(json_response[:total_on_hand]).to eq 6
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
context "stock_items" do
|
49
|
+
context "when given user" do
|
50
|
+
it "returns total that includes reserved stock"
|
51
|
+
end
|
52
|
+
context "when given no user" do
|
53
|
+
it "returns total that includes reserved stock"
|
54
|
+
end
|
55
|
+
context "when given a different user" do
|
56
|
+
it "returns total that includes reserved stock"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# Specs ReservedStockItem subclass to StockItem
|
4
|
+
describe Spree::ReservedStockItem, type: :model do
|
5
|
+
let(:reserved_stock_location) do
|
6
|
+
create(
|
7
|
+
:stock_location,
|
8
|
+
reserved_items: true,
|
9
|
+
propagate_all_variants: false,
|
10
|
+
backorderable_default: false
|
11
|
+
)
|
12
|
+
end
|
13
|
+
let(:user) { create(:user) }
|
14
|
+
let(:variant) { FactoryGirl.create(:variant) }
|
15
|
+
subject do
|
16
|
+
create(
|
17
|
+
:reserved_stock_item,
|
18
|
+
variant: variant,
|
19
|
+
stock_location: reserved_stock_location,
|
20
|
+
user: user,
|
21
|
+
backorderable: false
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
context "validation" do
|
26
|
+
context "stock location" do
|
27
|
+
it "can be valid" do
|
28
|
+
expect(subject).to be_valid
|
29
|
+
end
|
30
|
+
it "is invalid if backorderable" do
|
31
|
+
reserved_stock_item = build(
|
32
|
+
:reserved_stock_item,
|
33
|
+
variant: FactoryGirl.create(:variant),
|
34
|
+
stock_location: reserved_stock_location,
|
35
|
+
user: user,
|
36
|
+
backorderable: true
|
37
|
+
)
|
38
|
+
expect(reserved_stock_item).to be_invalid
|
39
|
+
end
|
40
|
+
it "is invalid if its stock_location is not for reserved_items" do
|
41
|
+
invalid_base_stock_location = create(
|
42
|
+
:stock_location,
|
43
|
+
reserved_items: false,
|
44
|
+
propagate_all_variants: false
|
45
|
+
)
|
46
|
+
reserved_stock_item = build(
|
47
|
+
:reserved_stock_item,
|
48
|
+
stock_location: invalid_base_stock_location,
|
49
|
+
backorderable: false
|
50
|
+
)
|
51
|
+
expect(reserved_stock_item).to be_invalid
|
52
|
+
end
|
53
|
+
end
|
54
|
+
context "user" do
|
55
|
+
it "is invalid without a user" do
|
56
|
+
reserved_stock_item = build(
|
57
|
+
:reserved_stock_item,
|
58
|
+
stock_location: reserved_stock_location,
|
59
|
+
user: nil,
|
60
|
+
backorderable: false
|
61
|
+
)
|
62
|
+
expect(reserved_stock_item).to be_invalid
|
63
|
+
end
|
64
|
+
it "is invalid if variant and user are not unique" do
|
65
|
+
reserved_stock_item = build(
|
66
|
+
:reserved_stock_item,
|
67
|
+
stock_location: reserved_stock_location,
|
68
|
+
variant: subject.variant,
|
69
|
+
user: subject.user,
|
70
|
+
backorderable: false
|
71
|
+
)
|
72
|
+
expect(reserved_stock_item).to be_invalid
|
73
|
+
end
|
74
|
+
it "is valid if variant is not unique, but user is" do
|
75
|
+
different_user = create(:user)
|
76
|
+
reserved_stock_item = build(
|
77
|
+
:reserved_stock_item,
|
78
|
+
stock_location: reserved_stock_location,
|
79
|
+
variant: subject.variant,
|
80
|
+
user: different_user,
|
81
|
+
backorderable: false
|
82
|
+
)
|
83
|
+
expect(reserved_stock_item).to be_valid
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
context "DB constraints" do
|
88
|
+
it "can save two items with same variant but different user" do
|
89
|
+
different_user = create(:user)
|
90
|
+
reserved_stock_item = build(
|
91
|
+
:reserved_stock_item,
|
92
|
+
stock_location: reserved_stock_location,
|
93
|
+
variant: subject.variant,
|
94
|
+
user: different_user,
|
95
|
+
backorderable: false
|
96
|
+
)
|
97
|
+
reserved_stock_item.save
|
98
|
+
expect(Spree::ReservedStockItem.count).to eq 2
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Spree::Stock::Coordinator, type: :model do
|
4
|
+
# Force setting up a normal stock lcoation before we create variant,
|
5
|
+
# otherwise we get too many stock locations
|
6
|
+
let!(:normal_stock_location) { create(:stock_location) }
|
7
|
+
let(:variant) { create(:variant) }
|
8
|
+
let(:user) { create(:user) }
|
9
|
+
let(:other_user) { create(:user) }
|
10
|
+
let(:reserved_stock_location) { Spree::StockLocation.reserved_items_location }
|
11
|
+
let(:normal_stock_item) do
|
12
|
+
normal_stock_location.stock_item(variant)
|
13
|
+
end
|
14
|
+
let(:reserved_stock_item_for_user) do
|
15
|
+
create(
|
16
|
+
:reserved_stock_item,
|
17
|
+
backorderable: false,
|
18
|
+
original_stock_location: normal_stock_location,
|
19
|
+
stock_location: reserved_stock_location,
|
20
|
+
user: user,
|
21
|
+
variant: variant
|
22
|
+
)
|
23
|
+
end
|
24
|
+
let(:reserved_stock_item_for_other_user) do
|
25
|
+
create(
|
26
|
+
:reserved_stock_item,
|
27
|
+
backorderable: false,
|
28
|
+
original_stock_location: normal_stock_location,
|
29
|
+
stock_location: reserved_stock_location,
|
30
|
+
user: other_user,
|
31
|
+
variant: variant
|
32
|
+
)
|
33
|
+
end
|
34
|
+
let(:order) { Spree::Order.new(user: user) }
|
35
|
+
subject { Spree::Stock::Coordinator.new(order) }
|
36
|
+
|
37
|
+
context "#packages" do
|
38
|
+
it "uses items reserved for the user first" do
|
39
|
+
reserved_stock_item_for_user.set_count_on_hand(5)
|
40
|
+
normal_stock_item.set_count_on_hand(5)
|
41
|
+
Spree::OrderContents.new(order).add(variant, 2)
|
42
|
+
packages = subject.packages
|
43
|
+
expect(packages.count).to eq 1
|
44
|
+
expect(packages.first.stock_location).to eq reserved_stock_location
|
45
|
+
expect(packages.first.contents.count).to eq 2
|
46
|
+
end
|
47
|
+
it "uses non-reserved items if there aren't enough reserved items" do
|
48
|
+
reserved_stock_item_for_user.set_count_on_hand(5)
|
49
|
+
normal_stock_item.set_count_on_hand(5)
|
50
|
+
Spree::OrderContents.new(order).add(variant, 7)
|
51
|
+
packages = subject.packages
|
52
|
+
expect(packages.count).to eq 2
|
53
|
+
expect(packages.first.stock_location).to eq reserved_stock_location
|
54
|
+
expect(packages.first.contents.count).to eq 5
|
55
|
+
expect(packages.second.stock_location).to eq normal_stock_location
|
56
|
+
expect(packages.second.contents.count).to eq 2
|
57
|
+
end
|
58
|
+
it "doesn't use items reserved by a different user" do
|
59
|
+
reserved_stock_item_for_other_user.set_count_on_hand(5)
|
60
|
+
normal_stock_item.set_count_on_hand(5)
|
61
|
+
Spree::OrderContents.new(order).add(variant, 2)
|
62
|
+
packages = subject.packages
|
63
|
+
expect(packages.count).to eq 1
|
64
|
+
expect(packages.first.stock_location).to eq normal_stock_location
|
65
|
+
expect(packages.first.contents.count).to eq 2
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
module Stock
|
5
|
+
describe Prioritizer, type: :model do
|
6
|
+
let(:original_stock_location) { create(:stock_location) }
|
7
|
+
let(:reserved_stock_location) { Spree::StockLocation.reserved_items_location }
|
8
|
+
let(:variant) { build(:variant) }
|
9
|
+
|
10
|
+
it "sorts packages from reserved stock location first" do
|
11
|
+
regular_package = Package.new(original_stock_location)
|
12
|
+
regular_inventory_unit = mock_model(InventoryUnit, variant: variant)
|
13
|
+
regular_package.add regular_inventory_unit
|
14
|
+
|
15
|
+
reserved_package = Package.new(reserved_stock_location)
|
16
|
+
reserved_inventory_unit = mock_model(InventoryUnit, variant: variant)
|
17
|
+
reserved_package.add reserved_inventory_unit
|
18
|
+
|
19
|
+
inventory_units = [regular_inventory_unit, reserved_inventory_unit]
|
20
|
+
packages = [regular_package, reserved_package]
|
21
|
+
prioritizer = Prioritizer.new(inventory_units, packages)
|
22
|
+
packages = prioritizer.prioritized_packages
|
23
|
+
|
24
|
+
expect(packages.first).to eq reserved_package
|
25
|
+
expect(packages.second).to eq regular_package
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|