diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index a039ddbd45..4a2be00f6e 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -41,4 +41,8 @@ Spree::AppConfiguration.class_eval do # Enable cache preference :enable_products_cache?, :boolean, default: (Rails.env.production? || Rails.env.staging?) + + def package_factory + @package_factory ||= OrderManagement::Stock::Package + end end diff --git a/app/models/spree/stock/package.rb b/app/models/spree/stock/package.rb deleted file mode 100644 index e724b3efa1..0000000000 --- a/app/models/spree/stock/package.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -module Spree - module Stock - class Package - ContentItem = Struct.new(:variant, :quantity, :state) - - attr_reader :stock_location, :order, :contents - attr_accessor :shipping_rates - - def initialize(stock_location, order, contents = []) - @stock_location = stock_location - @order = order - @contents = contents - @shipping_rates = [] - end - - def add(variant, quantity, state = :on_hand) - contents << ContentItem.new(variant, quantity, state) - end - - def weight - contents.sum { |item| item.variant.weight * item.quantity } - end - - def on_hand - contents.select { |item| item.state == :on_hand } - end - - def backordered - contents.select { |item| item.state == :backordered } - end - - def find_item(variant, state = :on_hand) - contents.select do |item| - item.variant == variant && - item.state == state - end.first - end - - def quantity(state = nil) - case state - when :on_hand - on_hand.sum(&:quantity) - when :backordered - backordered.sum(&:quantity) - else - contents.sum(&:quantity) - end - end - - def empty? - quantity.zero? - end - - def flattened - flat = [] - contents.each do |item| - item.quantity.times do - flat << ContentItem.new(item.variant, 1, item.state) - end - end - flat - end - - def flattened=(flattened) - contents.clear - flattened.each do |item| - current_item = find_item(item.variant, item.state) - if current_item - current_item.quantity += 1 - else - add(item.variant, item.quantity, item.state) - end - end - end - - def currency - # TODO calculate from first variant? - end - - def shipping_categories - contents.map { |item| item.variant.shipping_category }.compact.uniq - end - - def shipping_methods - shipping_categories.map(&:shipping_methods).flatten.uniq - end - - def inspect - out = "#{order} - " - out << contents.map do |content_item| - "#{content_item.variant.name} #{content_item.quantity} #{content_item.state}" - end.join('/') - out - end - - def to_shipment - shipment = Spree::Shipment.new - shipment.order = order - shipment.stock_location = stock_location - shipment.shipping_rates = shipping_rates - - contents.each do |item| - item.quantity.times do - unit = shipment.inventory_units.build - unit.pending = true - unit.order = order - unit.variant = item.variant - unit.state = item.state.to_s - end - end - - shipment - end - end - end -end diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index d331a0567f..6ff230c5f5 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -30,7 +30,6 @@ Spree.config do |config| config.auto_capture = true #config.override_actionmailer_config = false - config.package_factory = OrderManagement::Stock::Package config.order_updater_decorator = OrderUpdater # S3 settings diff --git a/engines/order_management/app/services/order_management/stock/package.rb b/engines/order_management/app/services/order_management/stock/package.rb index 6397c6d90e..3009d0b19c 100644 --- a/engines/order_management/app/services/order_management/stock/package.rb +++ b/engines/order_management/app/services/order_management/stock/package.rb @@ -1,22 +1,87 @@ # frozen_string_literal: true -# Extends Spree's Package implementation to skip shipping methods that are not -# valid for OFN. -# -# It requires the following configuration in config/initializers/spree.rb: -# -# Spree.config do |config| -# ... -# config.package_factory = OrderManagement::Stock::Package -# end -# module OrderManagement module Stock - class Package < Spree::Stock::Package + class Package + ContentItem = Struct.new(:variant, :quantity, :state) + + attr_reader :stock_location, :order, :contents + attr_accessor :shipping_rates + + def initialize(stock_location, order, contents = []) + @stock_location = stock_location + @order = order + @contents = contents + @shipping_rates = [] + end + + def add(variant, quantity, state = :on_hand) + contents << ContentItem.new(variant, quantity, state) + end + + def weight + contents.sum { |item| item.variant.weight * item.quantity } + end + + def on_hand + contents.select { |item| item.state == :on_hand } + end + + def backordered + contents.select { |item| item.state == :backordered } + end + + def find_item(variant, state = :on_hand) + contents.select do |item| + item.variant == variant && + item.state == state + end.first + end + + def quantity(state = nil) + case state + when :on_hand + on_hand.sum(&:quantity) + when :backordered + backordered.sum(&:quantity) + else + contents.sum(&:quantity) + end + end + + def empty? + quantity.zero? + end + + def flattened + flat = [] + contents.each do |item| + item.quantity.times do + flat << ContentItem.new(item.variant, 1, item.state) + end + end + flat + end + + def flattened=(flattened) + contents.clear + flattened.each do |item| + current_item = find_item(item.variant, item.state) + if current_item + current_item.quantity += 1 + else + add(item.variant, item.quantity, item.state) + end + end + end + + def currency + # TODO calculate from first variant? + end + # Returns all existing shipping categories. - # It does not filter by the shipping categories of the products in the order: it allows - # checkout of products with categories that are not the shipping method's categories # It disables the matching of product shipping category with shipping method's category + # It allows checkout of products with categories that are not the ship method's categories # # @return [Array] def shipping_categories @@ -27,13 +92,40 @@ module OrderManagement # # @return [Array] def shipping_methods - available_shipping_methods = super.to_a + available_shipping_methods = shipping_categories.map(&:shipping_methods).flatten.uniq.to_a available_shipping_methods.keep_if do |shipping_method| ships_with?(order.distributor.shipping_methods.to_a, shipping_method) end end + def inspect + out = "#{order} - " + out << contents.map do |content_item| + "#{content_item.variant.name} #{content_item.quantity} #{content_item.state}" + end.join('/') + out + end + + def to_shipment + shipment = Spree::Shipment.new + shipment.order = order + shipment.stock_location = stock_location + shipment.shipping_rates = shipping_rates + + contents.each do |item| + item.quantity.times do + unit = shipment.inventory_units.build + unit.pending = true + unit.order = order + unit.variant = item.variant + unit.state = item.state.to_s + end + end + + shipment + end + private # Checks whether the given distributor provides the specified shipping method diff --git a/engines/order_management/spec/services/order_management/stock/package_spec.rb b/engines/order_management/spec/services/order_management/stock/package_spec.rb index dd06edb775..205d50ae1a 100644 --- a/engines/order_management/spec/services/order_management/stock/package_spec.rb +++ b/engines/order_management/spec/services/order_management/stock/package_spec.rb @@ -1,58 +1,171 @@ +# frozen_string_literal: true + require 'spec_helper' module OrderManagement module Stock describe Package do - let(:stock_location) { double(:stock_location) } + context "base tests" do + let(:variant) { build(:variant, weight: 25.0) } + let(:stock_location) { build(:stock_location) } + let(:distributor) { create(:enterprise) } + let(:order) { build(:order, distributor: distributor) } - subject(:package) { Package.new(stock_location, order, contents) } + subject { Package.new(stock_location, order) } - let(:enterprise) { create(:enterprise) } - let(:other_enterprise) { create(:enterprise) } + it 'calculates the weight of all the contents' do + subject.add variant, 4 + expect(subject.weight).to eq 100.0 + end - let(:order) { build(:order, distributor: enterprise) } + it 'filters by on_hand and backordered' do + subject.add variant, 4, :on_hand + subject.add variant, 3, :backordered + expect(subject.on_hand.count).to eq 1 + expect(subject.backordered.count).to eq 1 + end - let(:variant1) do - instance_double( - Spree::Variant, - shipping_category: shipping_method1.shipping_categories.first - ) - end - let(:variant2) do - instance_double( - Spree::Variant, - shipping_category: shipping_method2.shipping_categories.first - ) - end - let(:variant3) do - instance_double(Spree::Variant, shipping_category: nil) - end + it 'calculates the quantity by state' do + subject.add variant, 4, :on_hand + subject.add variant, 3, :backordered - let(:contents) do - [ - Package::ContentItem.new(variant1, 1), - Package::ContentItem.new(variant1, 1), - Package::ContentItem.new(variant2, 1), - Package::ContentItem.new(variant3, 1) - ] - end + expect(subject.quantity).to eq 7 + expect(subject.quantity(:on_hand)).to eq 4 + expect(subject.quantity(:backordered)).to eq 3 + end - let(:shipping_method1) { create(:shipping_method, distributors: [enterprise]) } - let(:shipping_method2) { create(:shipping_method, distributors: [other_enterprise]) } + it 'returns nil for content item not found' do + item = subject.find_item(variant, :on_hand) + expect(item).to be_nil + end - describe '#shipping_methods' do - it 'does not return shipping methods not used by the package\'s order distributor' do - expect(package.shipping_methods).to eq [shipping_method1] + it 'finds content item for a variant' do + subject.add variant, 4, :on_hand + item = subject.find_item(variant, :on_hand) + expect(item.quantity).to eq 4 + end + + it 'get flattened contents' do + subject.add variant, 4, :on_hand + subject.add variant, 2, :backordered + flattened = subject.flattened + expect(flattened.select { |i| i.state == :on_hand }.size).to eq 4 + expect(flattened.select { |i| i.state == :backordered }.size).to eq 2 + end + + it 'set contents from flattened' do + flattened = [Package::ContentItem.new(variant, 1, :on_hand), + Package::ContentItem.new(variant, 1, :on_hand), + Package::ContentItem.new(variant, 1, :backordered), + Package::ContentItem.new(variant, 1, :backordered)] + + subject.flattened = flattened + expect(subject.on_hand.size).to eq 1 + expect(subject.on_hand.first.quantity).to eq 2 + + expect(subject.backordered.size).to eq 1 + end + + # Contains regression test for #2804 + it 'builds a list of shipping methods from all categories' do + shipping_method1 = create(:shipping_method, distributors: [distributor]) + shipping_method2 = create(:shipping_method, distributors: [distributor]) + variant1 = create(:variant, + shipping_category: shipping_method1.shipping_categories.first) + variant2 = create(:variant, + shipping_category: shipping_method2.shipping_categories.first) + variant3 = create(:variant, shipping_category: nil) + contents = [Package::ContentItem.new(variant1, 1), + Package::ContentItem.new(variant1, 1), + Package::ContentItem.new(variant2, 1), + Package::ContentItem.new(variant3, 1)] + + package = Package.new(stock_location, order, contents) + expect(package.shipping_methods.size).to eq 2 + end + + it "can convert to a shipment" do + flattened = [Package::ContentItem.new(variant, 2, :on_hand), + Package::ContentItem.new(variant, 1, :backordered)] + subject.flattened = flattened + + shipping_method = build(:shipping_method) + subject.shipping_rates = [ + Spree::ShippingRate.new(shipping_method: shipping_method, cost: 10.00, selected: true) + ] + + shipment = subject.to_shipment + expect(shipment.order).to eq subject.order + expect(shipment.stock_location).to eq subject.stock_location + expect(shipment.inventory_units.size).to eq 3 + + first_unit = shipment.inventory_units.first + expect(first_unit.variant).to eq variant + expect(first_unit.state).to eq 'on_hand' + expect(first_unit.order).to eq subject.order + expect(first_unit).to be_pending + + last_unit = shipment.inventory_units.last + expect(last_unit.variant).to eq variant + expect(last_unit.state).to eq 'backordered' + expect(last_unit.order).to eq subject.order + + expect(shipment.shipping_method).to eq shipping_method end end - describe '#shipping_categories' do - it "returns shipping categories that are not shipping categories of the order's products" do - package - other_shipping_category = Spree::ShippingCategory.create(name: "Custom") + context "#shipping_methods and #shipping_categories" do + let(:stock_location) { double(:stock_location) } - expect(package.shipping_categories).to eq [shipping_method1.shipping_categories.first, - other_shipping_category] + subject(:package) { Package.new(stock_location, order, contents) } + + let(:enterprise) { create(:enterprise) } + let(:other_enterprise) { create(:enterprise) } + + let(:order) { build(:order, distributor: enterprise) } + + let(:variant1) do + instance_double( + Spree::Variant, + shipping_category: shipping_method1.shipping_categories.first + ) + end + let(:variant2) do + instance_double( + Spree::Variant, + shipping_category: shipping_method2.shipping_categories.first + ) + end + let(:variant3) do + instance_double(Spree::Variant, shipping_category: nil) + end + + let(:contents) do + [ + Package::ContentItem.new(variant1, 1), + Package::ContentItem.new(variant1, 1), + Package::ContentItem.new(variant2, 1), + Package::ContentItem.new(variant3, 1) + ] + end + + let(:shipping_method1) { create(:shipping_method, distributors: [enterprise]) } + let(:shipping_method2) { create(:shipping_method, distributors: [other_enterprise]) } + + describe '#shipping_methods' do + it 'does not return shipping methods not used by the package\'s order distributor' do + expect(package.shipping_methods).to eq [shipping_method1] + end + end + + describe '#shipping_categories' do + it "returns ship categories that are not the ship categories of the order's products" do + package + other_shipping_category = Spree::ShippingCategory.create(name: "Custom") + + expect(package.shipping_categories).to eq [shipping_method1.shipping_categories.first, + other_shipping_category] + end end end end diff --git a/spec/models/spree/stock/package_spec.rb b/spec/models/spree/stock/package_spec.rb deleted file mode 100644 index 6b27b405ac..0000000000 --- a/spec/models/spree/stock/package_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -module Spree - module Stock - describe Package do - let(:variant) { build(:variant, weight: 25.0) } - let(:stock_location) { build(:stock_location) } - let(:order) { build(:order) } - - subject { Package.new(stock_location, order) } - - it 'calculates the weight of all the contents' do - subject.add variant, 4 - expect(subject.weight).to eq 100.0 - end - - it 'filters by on_hand and backordered' do - subject.add variant, 4, :on_hand - subject.add variant, 3, :backordered - expect(subject.on_hand.count).to eq 1 - expect(subject.backordered.count).to eq 1 - end - - it 'calculates the quantity by state' do - subject.add variant, 4, :on_hand - subject.add variant, 3, :backordered - - expect(subject.quantity).to eq 7 - expect(subject.quantity(:on_hand)).to eq 4 - expect(subject.quantity(:backordered)).to eq 3 - end - - it 'returns nil for content item not found' do - item = subject.find_item(variant, :on_hand) - expect(item).to be_nil - end - - it 'finds content item for a variant' do - subject.add variant, 4, :on_hand - item = subject.find_item(variant, :on_hand) - expect(item.quantity).to eq 4 - end - - it 'get flattened contents' do - subject.add variant, 4, :on_hand - subject.add variant, 2, :backordered - flattened = subject.flattened - expect(flattened.select { |i| i.state == :on_hand }.size).to eq 4 - expect(flattened.select { |i| i.state == :backordered }.size).to eq 2 - end - - it 'set contents from flattened' do - flattened = [Package::ContentItem.new(variant, 1, :on_hand), - Package::ContentItem.new(variant, 1, :on_hand), - Package::ContentItem.new(variant, 1, :backordered), - Package::ContentItem.new(variant, 1, :backordered)] - - subject.flattened = flattened - expect(subject.on_hand.size).to eq 1 - expect(subject.on_hand.first.quantity).to eq 2 - - expect(subject.backordered.size).to eq 1 - end - - # Contains regression test for #2804 - it 'builds a list of shipping methods from all categories' do - shipping_method1 = create(:shipping_method) - shipping_method2 = create(:shipping_method) - variant1 = create(:variant, - shipping_category: shipping_method1.shipping_categories.first) - variant2 = create(:variant, - shipping_category: shipping_method2.shipping_categories.first) - variant3 = create(:variant, shipping_category: nil) - contents = [Package::ContentItem.new(variant1, 1), - Package::ContentItem.new(variant1, 1), - Package::ContentItem.new(variant2, 1), - Package::ContentItem.new(variant3, 1)] - - package = Package.new(stock_location, order, contents) - expect(package.shipping_methods.size).to eq 2 - end - - it "can convert to a shipment" do - flattened = [Package::ContentItem.new(variant, 2, :on_hand), - Package::ContentItem.new(variant, 1, :backordered)] - subject.flattened = flattened - - shipping_method = build(:shipping_method) - subject.shipping_rates = [ - Spree::ShippingRate.new(shipping_method: shipping_method, cost: 10.00, selected: true) - ] - - shipment = subject.to_shipment - expect(shipment.order).to eq subject.order - expect(shipment.stock_location).to eq subject.stock_location - expect(shipment.inventory_units.size).to eq 3 - - first_unit = shipment.inventory_units.first - expect(first_unit.variant).to eq variant - expect(first_unit.state).to eq 'on_hand' - expect(first_unit.order).to eq subject.order - expect(first_unit).to be_pending - - last_unit = shipment.inventory_units.last - expect(last_unit.variant).to eq variant - expect(last_unit.state).to eq 'backordered' - expect(last_unit.order).to eq subject.order - - expect(shipment.shipping_method).to eq shipping_method - end - end - end -end diff --git a/spec/models/spree/stock/packer_spec.rb b/spec/models/spree/stock/packer_spec.rb index fb20808644..c5bdefd489 100644 --- a/spec/models/spree/stock/packer_spec.rb +++ b/spec/models/spree/stock/packer_spec.rb @@ -39,19 +39,6 @@ module Spree expect(package.backordered.size).to eq 5 end - context 'when a packer factory is not specified' do - let(:package) { double(:package, add: true) } - - it 'calls Spree::Stock::Package' do - expect(Package) - .to receive(:new) - .with(stock_location, order) - .and_return(package) - - subject.default_package - end - end - context 'when a packer factory is specified' do before do allow(Spree::Config).to receive(:package_factory) { TestPackageFactory }