diff --git a/core/app/models/spree/order_contents.rb b/core/app/models/spree/order_contents.rb index 5151df63030..580e72387bc 100644 --- a/core/app/models/spree/order_contents.rb +++ b/core/app/models/spree/order_contents.rb @@ -1,39 +1,9 @@ # frozen_string_literal: true module Spree - class OrderContents - attr_accessor :order - - def initialize(order) - @order = order - end - - # Add a line items to the order if there is inventory to do so - # and populate Promotions - # - # @param [Spree::Variant] variant The variant the line_item should - # be associated with - # @param [Integer] quantity The line_item quantity - # @param [Hash] options Options for the adding proccess - # Valid options: - # shipment: [Spree::Shipment] LineItem target shipment - # - # @return [Spree::LineItem] - def add(variant, quantity = 1, options = {}) - line_item = add_to_line_item(variant, quantity, options) - after_add_or_remove(line_item, options) - end - - def remove(variant, quantity = 1, options = {}) - line_item = remove_from_line_item(variant, quantity, options) - after_add_or_remove(line_item, options) - end - - def remove_line_item(line_item, options = {}) - order.line_items.destroy(line_item) - after_add_or_remove(line_item, options) - end - + class OrderContents < Spree::SimpleOrderContents + # Updates the order's line items with the params passed in. + # Also runs the PromotionHandler::Cart. def update_cart(params) if order.update(params) unless order.completed? @@ -43,7 +13,7 @@ def update_cart(params) # promotion rules would not be triggered. reload_totals order.check_shipments_and_restart_checkout - PromotionHandler::Cart.new(order).activate + ::Spree::PromotionHandler::Cart.new(order).activate end reload_totals true @@ -52,76 +22,15 @@ def update_cart(params) end end - def advance - while @order.next; end - end - - def approve(user: nil, name: nil) - if user.blank? && name.blank? - raise ArgumentError, 'user or name must be specified' - end - - order.update!( - approver: user, - approver_name: name, - approved_at: Time.current - ) - end - private def after_add_or_remove(line_item, options = {}) reload_totals shipment = options[:shipment] shipment.present? ? shipment.update_amounts : order.check_shipments_and_restart_checkout - PromotionHandler::Cart.new(order, line_item).activate + ::Spree::PromotionHandler::Cart.new(order, line_item).activate reload_totals line_item end - - def reload_totals - @order.recalculate - end - - def add_to_line_item(variant, quantity, options = {}) - line_item = grab_line_item_by_variant(variant, false, options) - - line_item ||= order.line_items.new( - quantity: 0, - variant: variant, - adjustments: [], - ) - - line_item.quantity += quantity.to_i - line_item.options = ActionController::Parameters.new(options).permit(PermittedAttributes.line_item_attributes).to_h - - line_item.target_shipment = options[:shipment] - line_item.save! - line_item - end - - def remove_from_line_item(variant, quantity, options = {}) - line_item = grab_line_item_by_variant(variant, true, options) - line_item.quantity -= quantity - line_item.target_shipment = options[:shipment] - - if line_item.quantity <= 0 - order.line_items.destroy(line_item) - else - line_item.save! - end - - line_item - end - - def grab_line_item_by_variant(variant, raise_error = false, options = {}) - line_item = order.find_line_item_by_variant(variant, options) - - if !line_item.present? && raise_error - raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}" - end - - line_item - end end end diff --git a/core/app/models/spree/simple_order_contents.rb b/core/app/models/spree/simple_order_contents.rb new file mode 100644 index 00000000000..6e50556342e --- /dev/null +++ b/core/app/models/spree/simple_order_contents.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module Spree + class SimpleOrderContents + attr_accessor :order + + def initialize(order) + @order = order + end + + # Add a line items to the order if there is inventory to do so + # and populate Promotions + # + # @param [Spree::Variant] variant The variant the line_item should + # be associated with + # @param [Integer] quantity The line_item quantity + # @param [Hash] options Options for the adding proccess + # Valid options: + # shipment: [Spree::Shipment] LineItem target shipment + # + # @return [Spree::LineItem] + def add(variant, quantity = 1, options = {}) + line_item = add_to_line_item(variant, quantity, options) + after_add_or_remove(line_item, options) + end + + def remove(variant, quantity = 1, options = {}) + line_item = remove_from_line_item(variant, quantity, options) + after_add_or_remove(line_item, options) + end + + def remove_line_item(line_item, options = {}) + order.line_items.destroy(line_item) + after_add_or_remove(line_item, options) + end + + def update_cart(params) + if order.update(params) + unless order.completed? + order.line_items = order.line_items.select { |li| li.quantity > 0 } + order.check_shipments_and_restart_checkout + end + reload_totals + true + else + false + end + end + + def advance + while @order.next; end + end + + def approve(user: nil, name: nil) + if user.blank? && name.blank? + raise ArgumentError, 'user or name must be specified' + end + + order.update!( + approver: user, + approver_name: name, + approved_at: Time.current + ) + end + + private + + def after_add_or_remove(line_item, options = {}) + shipment = options[:shipment] + shipment.present? ? shipment.update_amounts : order.check_shipments_and_restart_checkout + reload_totals + line_item + end + + def reload_totals + @order.recalculate + end + + def add_to_line_item(variant, quantity, options = {}) + line_item = grab_line_item_by_variant(variant, false, options) + + line_item ||= order.line_items.new( + quantity: 0, + variant: variant, + adjustments: [], + ) + + line_item.quantity += quantity.to_i + line_item.options = ActionController::Parameters.new(options).permit(PermittedAttributes.line_item_attributes).to_h + + line_item.target_shipment = options[:shipment] + line_item.save! + line_item + end + + def remove_from_line_item(variant, quantity, options = {}) + line_item = grab_line_item_by_variant(variant, true, options) + line_item.quantity -= quantity + line_item.target_shipment = options[:shipment] + + if line_item.quantity <= 0 + order.line_items.destroy(line_item) + else + line_item.save! + end + + line_item + end + + def grab_line_item_by_variant(variant, raise_error = false, options = {}) + line_item = order.find_line_item_by_variant(variant, options) + + if !line_item.present? && raise_error + raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}" + end + + line_item + end + end +end diff --git a/core/spec/models/spree/order_contents_spec.rb b/core/spec/models/spree/order_contents_spec.rb index eb34512f870..472a00d49a3 100644 --- a/core/spec/models/spree/order_contents_spec.rb +++ b/core/spec/models/spree/order_contents_spec.rb @@ -12,76 +12,6 @@ subject(:order_contents) { described_class.new(order) } context "#add" do - context 'given quantity is not explicitly provided' do - it 'should add one line item' do - line_item = subject.add(variant) - expect(line_item.quantity).to eq(1) - expect(order.line_items.size).to eq(1) - end - end - - context 'given a shipment' do - let!(:shipment) { create(:shipment, order: order) } - - it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do - expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) - expect(shipment).to receive(:update_amounts).at_least(:once) - subject.add(variant, 1, shipment: shipment) - end - - context "with quantity=1" do - it "creates correct inventory" do - subject.add(variant, 1, shipment: shipment) - expect(order.inventory_units.count).to eq(1) - end - end - - context "with quantity=2" do - it "creates correct inventory" do - subject.add(variant, 2, shipment: shipment) - expect(order.inventory_units.count).to eq(2) - end - end - - context "called multiple times" do - it "creates correct inventory" do - subject.add(variant, 1, shipment: shipment) - subject.add(variant, 1, shipment: shipment) - expect(order.inventory_units.count).to eq(2) - end - end - end - - context 'not given a shipment' do - it "ensures updated shipments" do - expect(subject.order).to receive(:check_shipments_and_restart_checkout) - subject.add(variant) - end - end - - it 'should add line item if one does not exist' do - line_item = subject.add(variant, 1) - expect(line_item.quantity).to eq(1) - expect(order.line_items.size).to eq(1) - end - - it 'should update line item if one exists' do - subject.add(variant, 1) - line_item = subject.add(variant, 1) - expect(line_item.quantity).to eq(2) - expect(order.line_items.size).to eq(1) - end - - it "should update order totals" do - expect(order.item_total.to_f).to eq(0.00) - expect(order.total.to_f).to eq(0.00) - - subject.add(variant, 1) - - expect(order.item_total.to_f).to eq(19.99) - expect(order.total.to_f).to eq(19.99) - end - context "running promotions" do let(:promotion) { create(:promotion, apply_automatically: true) } let(:calculator) { Spree::Calculator::FlatRate.new(preferred_amount: 10) } @@ -113,151 +43,6 @@ include_context "discount changes order total" end end - - describe 'tax calculations' do - let!(:zone) { create(:global_zone) } - let!(:tax_rate) do - create(:tax_rate, zone: zone, tax_categories: [variant.tax_category]) - end - - context 'when the order has a taxable address' do - before do - expect(order.tax_address.country_id).to be_present - end - - it 'creates a tax adjustment' do - order_contents.add(variant) - line_item = order.find_line_item_by_variant(variant) - expect(line_item.adjustments.tax.count).to eq(1) - end - end - - context 'when the order does not have a taxable address' do - before do - order.update!(ship_address: nil, bill_address: nil) - expect(order.tax_address.country_id).to be_nil - end - - it 'creates a tax adjustment' do - order_contents.add(variant) - line_item = order.find_line_item_by_variant(variant) - expect(line_item.adjustments.tax.count).to eq(0) - end - end - end - end - - context "#remove" do - context "given an invalid variant" do - it "raises an exception" do - expect { - subject.remove(variant, 1) - }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'given quantity is not explicitly provided' do - it 'should remove one line item' do - line_item = subject.add(variant, 3) - subject.remove(variant) - - expect(line_item.reload.quantity).to eq(2) - end - end - - context 'given a shipment' do - it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do - subject.add(variant, 1) - shipment = create(:shipment) - expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) - expect(shipment).to receive(:update_amounts) - subject.remove(variant, 1, shipment: shipment) - end - end - - context 'not given a shipment' do - it "ensures updated shipments" do - subject.add(variant, 1) - expect(subject.order).to receive(:check_shipments_and_restart_checkout) - subject.remove(variant) - end - end - - it 'should reduce line_item quantity if quantity is less the line_item quantity' do - line_item = subject.add(variant, 3) - subject.remove(variant, 1) - - expect(line_item.reload.quantity).to eq(2) - end - - it 'should remove line_item if quantity matches line_item quantity' do - subject.add(variant, 1) - subject.remove(variant, 1) - - expect(order.reload.find_line_item_by_variant(variant)).to be_nil - end - - it 'should remove line_item if quantity is greater than line_item quantity' do - subject.add(variant, 1) - subject.remove(variant, 2) - - expect(order.reload.find_line_item_by_variant(variant)).to be_nil - end - - it "should update order totals" do - expect(order.item_total.to_f).to eq(0.00) - expect(order.total.to_f).to eq(0.00) - - subject.add(variant, 2) - - expect(order.item_total.to_f).to eq(39.98) - expect(order.total.to_f).to eq(39.98) - - subject.remove(variant, 1) - expect(order.item_total.to_f).to eq(19.99) - expect(order.total.to_f).to eq(19.99) - end - end - - context "#remove_line_item" do - context 'given a shipment' do - it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do - line_item = subject.add(variant, 1) - shipment = create(:shipment) - expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) - expect(shipment).to receive(:update_amounts) - subject.remove_line_item(line_item, shipment: shipment) - end - end - - context 'not given a shipment' do - it "ensures updated shipments" do - line_item = subject.add(variant, 1) - expect(subject.order).to receive(:check_shipments_and_restart_checkout) - subject.remove_line_item(line_item) - end - end - - it 'should remove line_item' do - line_item = subject.add(variant, 1) - subject.remove_line_item(line_item) - - expect(order.reload.line_items).to_not include(line_item) - end - - it "should update order totals" do - expect(order.item_total.to_f).to eq(0.00) - expect(order.total.to_f).to eq(0.00) - - line_item = subject.add(variant, 2) - - expect(order.item_total.to_f).to eq(39.98) - expect(order.total.to_f).to eq(39.98) - - subject.remove_line_item(line_item) - expect(order.item_total.to_f).to eq(0.00) - expect(order.total.to_f).to eq(0.00) - end end context "update cart" do @@ -321,48 +106,4 @@ }.to change { order.payment_state } end end - - describe "#approve" do - context 'when a name is supplied' do - it 'approves the order' do - order.contents.approve(name: 'Jordan') - expect(order.approver).to be_nil - expect(order.approver_name).to eq('Jordan') - expect(order.approved_at).to be_present - expect(order.approved?).to be_truthy - end - end - - context 'when a user is supplied' do - let(:user) { create(:user) } - - it 'approves the order' do - order.contents.approve(user: user) - expect(order.approver).to eq(user) - expect(order.approver_name).to be_nil - expect(order.approved_at).to be_present - expect(order.approved?).to be_truthy - end - end - - context 'when a user and a name are supplied' do - let(:user) { create(:user) } - - it 'approves the order' do - order.contents.approve(user: user, name: 'Jordan') - expect(order.approver).to eq(user) - expect(order.approver_name).to eq('Jordan') - expect(order.approved_at).to be_present - expect(order.approved?).to be_truthy - end - end - - context 'when neither a user nor a name are supplied' do - it 'raises' do - expect { - order.contents.approve - }.to raise_error(ArgumentError, 'user or name must be specified') - end - end - end end diff --git a/core/spec/models/spree/simple_order_contents_spec.rb b/core/spec/models/spree/simple_order_contents_spec.rb new file mode 100644 index 00000000000..812b5fa2b8a --- /dev/null +++ b/core/spec/models/spree/simple_order_contents_spec.rb @@ -0,0 +1,336 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spree::SimpleOrderContents, type: :model do + let!(:store) { create :store } + let(:order) { create(:order) } + let(:variant) { create(:variant) } + let!(:stock_location) { variant.stock_locations.first } + let(:stock_location_2) { create(:stock_location) } + + subject(:order_contents) { described_class.new(order) } + + context "#add" do + context 'given quantity is not explicitly provided' do + it 'should add one line item' do + line_item = subject.add(variant) + expect(line_item.quantity).to eq(1) + expect(order.line_items.size).to eq(1) + end + end + + context 'given a shipment' do + let!(:shipment) { create(:shipment, order: order) } + + it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do + expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) + expect(shipment).to receive(:update_amounts).at_least(:once) + subject.add(variant, 1, shipment: shipment) + end + + context "with quantity=1" do + it "creates correct inventory" do + subject.add(variant, 1, shipment: shipment) + expect(order.inventory_units.count).to eq(1) + end + end + + context "with quantity=2" do + it "creates correct inventory" do + subject.add(variant, 2, shipment: shipment) + expect(order.inventory_units.count).to eq(2) + end + end + + context "called multiple times" do + it "creates correct inventory" do + subject.add(variant, 1, shipment: shipment) + subject.add(variant, 1, shipment: shipment) + expect(order.inventory_units.count).to eq(2) + end + end + end + + context 'not given a shipment' do + it "ensures updated shipments" do + expect(subject.order).to receive(:check_shipments_and_restart_checkout) + subject.add(variant) + end + end + + it 'should add line item if one does not exist' do + line_item = subject.add(variant, 1) + expect(line_item.quantity).to eq(1) + expect(order.line_items.size).to eq(1) + end + + it 'should update line item if one exists' do + subject.add(variant, 1) + line_item = subject.add(variant, 1) + expect(line_item.quantity).to eq(2) + expect(order.line_items.size).to eq(1) + end + + it "should update order totals" do + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + + subject.add(variant, 1) + + expect(order.item_total.to_f).to eq(19.99) + expect(order.total.to_f).to eq(19.99) + end + + describe 'tax calculations' do + let!(:zone) { create(:global_zone) } + let!(:tax_rate) do + create(:tax_rate, zone: zone, tax_categories: [variant.tax_category]) + end + + context 'when the order has a taxable address' do + before do + expect(order.tax_address.country_id).to be_present + end + + it 'creates a tax adjustment' do + order_contents.add(variant) + line_item = order.find_line_item_by_variant(variant) + expect(line_item.adjustments.tax.count).to eq(1) + end + end + + context 'when the order does not have a taxable address' do + before do + order.update!(ship_address: nil, bill_address: nil) + expect(order.tax_address.country_id).to be_nil + end + + it 'creates a tax adjustment' do + order_contents.add(variant) + line_item = order.find_line_item_by_variant(variant) + expect(line_item.adjustments.tax.count).to eq(0) + end + end + end + end + + context "#remove" do + context "given an invalid variant" do + it "raises an exception" do + expect { + subject.remove(variant, 1) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'given quantity is not explicitly provided' do + it 'should remove one line item' do + line_item = subject.add(variant, 3) + subject.remove(variant) + + expect(line_item.reload.quantity).to eq(2) + end + end + + context 'given a shipment' do + it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do + subject.add(variant, 1) + shipment = create(:shipment) + expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) + expect(shipment).to receive(:update_amounts) + subject.remove(variant, 1, shipment: shipment) + end + end + + context 'not given a shipment' do + it "ensures updated shipments" do + subject.add(variant, 1) + expect(subject.order).to receive(:check_shipments_and_restart_checkout) + subject.remove(variant) + end + end + + it 'should reduce line_item quantity if quantity is less the line_item quantity' do + line_item = subject.add(variant, 3) + subject.remove(variant, 1) + + expect(line_item.reload.quantity).to eq(2) + end + + it 'should remove line_item if quantity matches line_item quantity' do + subject.add(variant, 1) + subject.remove(variant, 1) + + expect(order.reload.find_line_item_by_variant(variant)).to be_nil + end + + it 'should remove line_item if quantity is greater than line_item quantity' do + subject.add(variant, 1) + subject.remove(variant, 2) + + expect(order.reload.find_line_item_by_variant(variant)).to be_nil + end + + it "should update order totals" do + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + + subject.add(variant, 2) + + expect(order.item_total.to_f).to eq(39.98) + expect(order.total.to_f).to eq(39.98) + + subject.remove(variant, 1) + expect(order.item_total.to_f).to eq(19.99) + expect(order.total.to_f).to eq(19.99) + end + end + + context "#remove_line_item" do + context 'given a shipment' do + it "ensure shipment calls update_amounts instead of order calling check_shipments_and_restart_checkout" do + line_item = subject.add(variant, 1) + shipment = create(:shipment) + expect(subject.order).to_not receive(:check_shipments_and_restart_checkout) + expect(shipment).to receive(:update_amounts) + subject.remove_line_item(line_item, shipment: shipment) + end + end + + context 'not given a shipment' do + it "ensures updated shipments" do + line_item = subject.add(variant, 1) + expect(subject.order).to receive(:check_shipments_and_restart_checkout) + subject.remove_line_item(line_item) + end + end + + it 'should remove line_item' do + line_item = subject.add(variant, 1) + subject.remove_line_item(line_item) + + expect(order.reload.line_items).to_not include(line_item) + end + + it "should update order totals" do + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + + line_item = subject.add(variant, 2) + + expect(order.item_total.to_f).to eq(39.98) + expect(order.total.to_f).to eq(39.98) + + subject.remove_line_item(line_item) + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + end + end + + context "update cart" do + let!(:shirt) { subject.add variant, 1 } + + let(:params) do + { line_items_attributes: { + "0" => { id: shirt.id, quantity: 3 } + } } + end + + it "changes item quantity" do + subject.update_cart params + expect(shirt.reload.quantity).to eq 3 + end + + it "updates order totals" do + expect { + subject.update_cart params + }.to change { subject.order.total } + end + + context "submits item quantity 0" do + let(:params) do + { line_items_attributes: { + "0" => { id: shirt.id, quantity: 0 } + } } + end + + it "removes item from order" do + expect { + subject.update_cart params + }.to change { subject.order.line_items.count } + end + end + + it "ensures updated shipments" do + expect(subject.order).to receive(:check_shipments_and_restart_checkout) + subject.update_cart params + end + end + + context "completed order" do + let(:order) do + Spree::Order.create!( + state: 'complete', + completed_at: Time.current, + email: "test@example.com" + ) + end + + before { order.shipments.create! stock_location_id: variant.stock_location_ids.first } + + it "updates order payment state" do + expect { + subject.add variant + }.to change { order.payment_state } + + expect { + subject.remove variant + }.to change { order.payment_state } + end + end + + describe "#approve" do + context 'when a name is supplied' do + it 'approves the order' do + order.contents.approve(name: 'Jordan') + expect(order.approver).to be_nil + expect(order.approver_name).to eq('Jordan') + expect(order.approved_at).to be_present + expect(order.approved?).to be_truthy + end + end + + context 'when a user is supplied' do + let(:user) { create(:user) } + + it 'approves the order' do + order.contents.approve(user: user) + expect(order.approver).to eq(user) + expect(order.approver_name).to be_nil + expect(order.approved_at).to be_present + expect(order.approved?).to be_truthy + end + end + + context 'when a user and a name are supplied' do + let(:user) { create(:user) } + + it 'approves the order' do + order.contents.approve(user: user, name: 'Jordan') + expect(order.approver).to eq(user) + expect(order.approver_name).to eq('Jordan') + expect(order.approved_at).to be_present + expect(order.approved?).to be_truthy + end + end + + context 'when neither a user nor a name are supplied' do + it 'raises' do + expect { + order.contents.approve + }.to raise_error(ArgumentError, 'user or name must be specified') + end + end + end +end diff --git a/legacy_promotions/lib/solidus_legacy_promotions/engine.rb b/legacy_promotions/lib/solidus_legacy_promotions/engine.rb index 2f897653ee9..b643b94d718 100644 --- a/legacy_promotions/lib/solidus_legacy_promotions/engine.rb +++ b/legacy_promotions/lib/solidus_legacy_promotions/engine.rb @@ -45,5 +45,9 @@ class Engine < ::Rails::Engine initializer "solidus_legacy_promotions.assets" do |app| app.config.assets.precompile << "solidus_legacy_promotions/manifest.js" end + + initializer "solidus_legacy_promotions", after: "spree.load_config_initializers" do + Spree::Config.order_contents_class = "Spree::OrderContents" + end end end