solidus_reserved_stock 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +21 -0
  5. data/.ruby-gemset +1 -0
  6. data/.travis.yml +4 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +21 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +74 -0
  11. data/Rakefile +26 -0
  12. data/app/controllers/spree/admin/reserved_stock_items_controller.rb +88 -0
  13. data/app/controllers/spree/admin/stock_items_controller_decorator.rb +7 -0
  14. data/app/controllers/spree/admin/users_controller_decorator.rb +16 -0
  15. data/app/controllers/spree/api/products_controller_decorator.rb +12 -0
  16. data/app/controllers/spree/api/v1/reserved_stock_items_controller.rb +88 -0
  17. data/app/controllers/spree/api/v1/validators/reserve_stock_params_validator.rb +37 -0
  18. data/app/controllers/spree/api/variants_controller_decorator.rb +12 -0
  19. data/app/helpers/spree/admin/reserved_stock_items_helper.rb +13 -0
  20. data/app/helpers/spree/admin/stock_locations_helper_decorator.rb +14 -0
  21. data/app/helpers/spree/api/api_helpers_decorator.rb +7 -0
  22. data/app/models/spree/product_decorator.rb +18 -0
  23. data/app/models/spree/reserved_stock_item.rb +48 -0
  24. data/app/models/spree/stock/coordinator_decorator.rb +28 -0
  25. data/app/models/spree/stock/prioritizer_decorator.rb +16 -0
  26. data/app/models/spree/stock/quantifier_decorator.rb +29 -0
  27. data/app/models/spree/stock/reserver.rb +60 -0
  28. data/app/models/spree/stock_location_decorator.rb +46 -0
  29. data/app/models/spree/stock_reservation_ability.rb +11 -0
  30. data/app/models/spree/user_decorator.rb +42 -0
  31. data/app/models/spree/variant_decorator.rb +6 -0
  32. data/app/overrides/spree/admin/stock_items/_stock_management/adjust_number_of_rows.html.erb.deface +2 -0
  33. data/app/overrides/spree/admin/stock_items/_stock_management/show_user_for_reserved_stock.html.erb.deface +2 -0
  34. data/app/overrides/spree/admin/users/_sidebar/add_reserved_stock_menu_item.html.erb.deface +6 -0
  35. data/app/views/spree/admin/reserved_stock_items/_form.html.erb +32 -0
  36. data/app/views/spree/admin/reserved_stock_items/index.html.erb +104 -0
  37. data/app/views/spree/admin/reserved_stock_items/new.html.erb +19 -0
  38. data/app/views/spree/api/products/show.v1.rabl +35 -0
  39. data/app/views/spree/api/v1/reserved_stock_items/index.v1.rabl +7 -0
  40. data/app/views/spree/api/v1/reserved_stock_items/show.v1.rabl +5 -0
  41. data/app/views/spree/api/variants/big.v1.rabl +20 -0
  42. data/app/views/spree/api/variants/small.v1.rabl +17 -0
  43. data/bin/console +14 -0
  44. data/bin/rails +12 -0
  45. data/bin/setup +7 -0
  46. data/config/i18n-tasks.yml +103 -0
  47. data/config/locales/en.yml +23 -0
  48. data/config/routes.rb +27 -0
  49. data/db/migrate/20160105203812_add_reserved_items_to_stock_location.rb +6 -0
  50. data/db/migrate/20160105222821_add_type_to_spree_stock_items.rb +7 -0
  51. data/db/migrate/20160106215753_add_user_id_to_spree_stock_items.rb +5 -0
  52. data/db/migrate/20160229223744_add_original_stock_location_id_to_spree_stock_items.rb +5 -0
  53. data/db/migrate/20160301003702_add_expires_at_to_spree_stock_items.rb +5 -0
  54. data/db/migrate/20160309025334_modify_stock_item_unique_index.rb +14 -0
  55. data/lib/generators/solidus_reserved_stock/install/install_generator.rb +22 -0
  56. data/lib/solidus_reserved_stock/ability_initializer.rb +5 -0
  57. data/lib/solidus_reserved_stock/engine.rb +15 -0
  58. data/lib/solidus_reserved_stock/version.rb +18 -0
  59. data/lib/solidus_reserved_stock.rb +7 -0
  60. data/lib/tasks/solidus_reserved_stock_tasks.rake +4 -0
  61. data/solidus_reserved_stock.gemspec +56 -0
  62. data/spec/controllers/spree/api/stock_locations_controller_spec.rb +24 -0
  63. data/spec/controllers/spree/api/v1/reserved_stock_items_controller_spec.rb +196 -0
  64. data/spec/controllers/spree/api/v1/validators/reserve_stock_params_validator_spec.rb +41 -0
  65. data/spec/controllers/spree/api/variants_controller_spec.rb +62 -0
  66. data/spec/factories/reserved_stock_item_factory.rb +8 -0
  67. data/spec/models/spree/reserved_stock_item_spec.rb +101 -0
  68. data/spec/models/spree/stock/coordinator_decorator_spec.rb +68 -0
  69. data/spec/models/spree/stock/prioritizer_decorator_spec.rb +29 -0
  70. data/spec/models/spree/stock/quantifier_decorator_spec.rb +42 -0
  71. data/spec/models/spree/stock/reserver_spec.rb +103 -0
  72. data/spec/models/spree/stock_location_decorator_spec.rb +47 -0
  73. data/spec/models/spree/user_decorator_spec.rb +65 -0
  74. data/spec/spec_helper.rb +56 -0
  75. data/spec/support/api_helpers.rb +35 -0
  76. data/spec/support/database_cleaner.rb +14 -0
  77. data/spec/support/have_attributes_matcher.rb +10 -0
  78. 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,5 @@
1
+ class AbilityInitializer < Rails::Railtie
2
+ initializer "solidus_reserved_stock.configure_rails_initialization" do
3
+ Spree::Ability.register_ability(Spree::StockReservationAbility)
4
+ end
5
+ 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,7 @@
1
+ require "solidus_core"
2
+ require "solidus_reserved_stock/engine"
3
+ require "solidus_reserved_stock/version"
4
+ require "solidus_reserved_stock/ability_initializer"
5
+
6
+ module SolidusReservedStock
7
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :solidus_reserved_stock do
3
+ # # Task goes here
4
+ # 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,8 @@
1
+ FactoryGirl.define do
2
+ factory :reserved_stock_item, class: Spree::ReservedStockItem do
3
+ backorderable true
4
+ stock_location
5
+ variant { FactoryGirl.create(:variant) }
6
+ after(:create) { |object| object.adjust_count_on_hand(10) }
7
+ end
8
+ 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