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.
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