solidus_reserved_stock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|