require 'spec_helper' require 'benchmark' describe Spree::Shipment, type: :model do let(:order) { mock_model Spree::Order, backordered?: false, canceled?: false, can_ship?: true, currency: 'USD', number: 'S12345', paid?: false, touch_later: false } let(:shipping_method) { create(:shipping_method, name: "UPS") } let(:shipment) do shipment = Spree::Shipment.new(cost: 1, state: 'pending', stock_location: create(:stock_location)) allow(shipment).to receive_messages order: order allow(shipment).to receive_messages shipping_method: shipping_method shipment.save shipment end let(:variant) { mock_model(Spree::Variant) } let(:line_item) { mock_model(Spree::LineItem, variant: variant) } def create_shipment(order, stock_location) order.shipments.create({ stock_location_id: stock_location.id }).inventory_units.create( order_id: order.id, variant_id: order.line_items.first.variant_id, line_item_id: order.line_items.first.id ) end describe "precision of pre_tax_amount" do let(:line_item) { create :line_item, pre_tax_amount: 4.2051 } it "keeps four digits of precision even when reloading" do # prevent it from updating pre_tax_amount allow_any_instance_of(Spree::LineItem).to receive(:update_tax_charge) expect(line_item.reload.pre_tax_amount).to eq(4.2051) end end # Regression test for #4063 context "number generation" do before do allow(order).to receive :update_with_updater! end it "generates a number containing a letter + 11 numbers" do expect(shipment.number[0]).to eq("H") expect(/\d{11}/.match(shipment.number)).not_to be_nil expect(shipment.number.length).to eq(12) end end it 'is backordered if one if its inventory_units is backordered' do allow(shipment).to receive_messages(inventory_units: [ mock_model(Spree::InventoryUnit, backordered?: false), mock_model(Spree::InventoryUnit, backordered?: true) ]) expect(shipment).to be_backordered end context '#determine_state' do it 'returns canceled if order is canceled?' do allow(order).to receive_messages canceled?: true expect(shipment.determine_state(order)).to eq 'canceled' end it 'returns pending unless order.can_ship?' do allow(order).to receive_messages can_ship?: false expect(shipment.determine_state(order)).to eq 'pending' end it 'returns pending if backordered' do allow(shipment).to receive_messages inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)] expect(shipment.determine_state(order)).to eq 'pending' end it 'returns shipped when already shipped' do allow(shipment).to receive_messages state: 'shipped' expect(shipment.determine_state(order)).to eq 'shipped' end it 'returns pending when unpaid' do expect(shipment.determine_state(order)).to eq 'pending' end it 'returns ready when paid' do allow(order).to receive_messages paid?: true expect(shipment.determine_state(order)).to eq 'ready' end it 'returns ready when Config.auto_capture_on_dispatch' do Spree::Config.auto_capture_on_dispatch = true expect(shipment.determine_state(order)).to eq 'ready' end end context "display_amount" do it "retuns a Spree::Money" do allow(shipment).to receive(:cost) { 21.22 } expect(shipment.display_amount).to eq(Spree::Money.new(21.22)) end end context "display_final_price" do it "retuns a Spree::Money" do allow(shipment).to receive(:final_price) { 21.22 } expect(shipment.display_final_price).to eq(Spree::Money.new(21.22)) end end context "display_item_cost" do it "retuns a Spree::Money" do allow(shipment).to receive(:item_cost) { 21.22 } expect(shipment.display_item_cost).to eq(Spree::Money.new(21.22)) end end context "#item_cost" do it 'should equal shipment line items amount with tax' do order = create(:order_with_line_item_quantity, line_items_quantity: 2) stock_location = create(:stock_location) create_shipment(order, stock_location) create_shipment(order, stock_location) create :tax_adjustment, adjustable: order.line_items.first, order: order expect(order.shipments.first.item_cost).to eql(11.0) expect(order.shipments.last.item_cost).to eql(11.0) end it 'should equal line items final amount with tax' do shipment = create(:shipment, order: create(:order_with_line_item_quantity, line_items_quantity: 2)) create :tax_adjustment, adjustable: shipment.order.line_items.first, order: shipment.order expect(shipment.item_cost).to eql(22.0) end end it "#discounted_cost" do shipment = create(:shipment) shipment.cost = 10 shipment.promo_total = -1 expect(shipment.discounted_cost).to eq(9) end it "#tax_total with included taxes" do shipment = Spree::Shipment.new expect(shipment.tax_total).to eq(0) shipment.included_tax_total = 10 expect(shipment.tax_total).to eq(10) end it "#tax_total with additional taxes" do shipment = Spree::Shipment.new expect(shipment.tax_total).to eq(0) shipment.additional_tax_total = 10 expect(shipment.tax_total).to eq(10) end it "#final_price" do shipment = Spree::Shipment.new shipment.cost = 10 shipment.adjustment_total = -2 shipment.included_tax_total = 1 expect(shipment.final_price).to eq(8) end context "manifest" do let(:order) { Spree::Order.create } let(:variant) { create(:variant) } let!(:line_item) { order.contents.add variant } let!(:shipment) { order.create_proposed_shipments.first } it "returns variant expected" do expect(shipment.manifest.first.variant).to eq variant end context "variant was removed" do before { variant.destroy } it "still returns variant expected" do expect(shipment.manifest.first.variant).to eq variant end end end context 'shipping_rates' do let(:shipment) { create(:shipment) } let(:shipping_method1) { create(:shipping_method) } let(:shipping_method2) { create(:shipping_method) } let(:shipping_rates) { [ Spree::ShippingRate.new(shipping_method: shipping_method1, cost: 10.00, selected: true), Spree::ShippingRate.new(shipping_method: shipping_method2, cost: 20.00) ] } it 'returns shipping_method from selected shipping_rate' do shipment.shipping_rates.delete_all shipment.shipping_rates.create shipping_method: shipping_method1, cost: 10.00, selected: true expect(shipment.shipping_method).to eq shipping_method1 end context 'refresh_rates' do let(:mock_estimator) { double('estimator', shipping_rates: shipping_rates) } before { allow(shipment).to receive(:can_get_rates?){ true } } it 'should request new rates, and maintain shipping_method selection' do expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator) allow(shipment).to receive_messages(shipping_method: shipping_method2) expect(shipment.refresh_rates).to eq(shipping_rates) expect(shipment.reload.selected_shipping_rate.shipping_method_id).to eq(shipping_method2.id) end it 'should handle no shipping_method selection' do expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator) allow(shipment).to receive_messages(shipping_method: nil) expect(shipment.refresh_rates).to eq(shipping_rates) expect(shipment.reload.selected_shipping_rate).not_to be_nil end it 'should not refresh if shipment is shipped' do expect(Spree::Stock::Estimator).not_to receive(:new) shipment.shipping_rates.delete_all allow(shipment).to receive_messages(shipped?: true) expect(shipment.refresh_rates).to eq([]) end it "can't get rates without a shipping address" do shipment.order.ship_address = nil expect(shipment.refresh_rates).to eq([]) end context 'to_package' do let(:inventory_units) do [build(:inventory_unit, line_item: line_item, variant: variant, state: 'on_hand'), build(:inventory_unit, line_item: line_item, variant: variant, state: 'backordered')] end before do allow(shipment).to receive(:inventory_units) { inventory_units } allow(inventory_units).to receive_message_chain(:includes, :joins).and_return inventory_units end it 'should use symbols for states when adding contents to package' do package = shipment.to_package expect(package.on_hand.count).to eq 1 expect(package.backordered.count).to eq 1 end end end end context "#update!" do shared_examples_for "immutable once shipped" do it "should remain in shipped state once shipped" do shipment.state = 'shipped' expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time)) shipment.update!(order) end end shared_examples_for "pending if backordered" do it "should have a state of pending if backordered" do allow(shipment).to receive_messages(inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)]) expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) shipment.update!(order) end end context "when order cannot ship" do before { allow(order).to receive_messages can_ship?: false } it "should result in a 'pending' state" do expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) shipment.update!(order) end end context "when order is paid" do before { allow(order).to receive_messages paid?: true } it "should result in a 'ready' state" do expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time)) shipment.update!(order) end it_should_behave_like 'immutable once shipped' it_should_behave_like 'pending if backordered' end context "when order has balance due" do before { allow(order).to receive_messages paid?: false } it "should result in a 'pending' state" do shipment.state = 'ready' expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) shipment.update!(order) end it_should_behave_like 'immutable once shipped' it_should_behave_like 'pending if backordered' end context "when order has a credit owed" do before { allow(order).to receive_messages payment_state: 'credit_owed', paid?: true } it "should result in a 'ready' state" do shipment.state = 'pending' expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time)) shipment.update!(order) end it_should_behave_like 'immutable once shipped' it_should_behave_like 'pending if backordered' end context "when shipment state changes to shipped" do before do allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) end it "should call after_ship" do shipment.state = 'pending' expect(shipment).to receive :after_ship allow(shipment).to receive_messages determine_state: 'shipped' expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time)) shipment.update!(order) end context "when using the default shipment handler" do it "should call the 'perform' method" do shipment.state = 'pending' allow(shipment).to receive_messages determine_state: 'shipped' expect_any_instance_of(Spree::ShipmentHandler).to receive(:perform) shipment.update!(order) end end context "when using a custom shipment handler" do before do Spree::ShipmentHandler::UPS = Class.new { def initialize(shipment) true end def perform() true end } end it "should call the custom handler's 'perform' method" do shipment.state = 'pending' allow(shipment).to receive_messages determine_state: 'shipped' expect_any_instance_of(Spree::ShipmentHandler::UPS).to receive(:perform) shipment.update!(order) end after do Spree::ShipmentHandler.send(:remove_const, :UPS) end end # Regression test for #4347 context "with adjustments" do before do shipment.adjustments << Spree::Adjustment.create(order: order, label: "Label", amount: 5) end it "transitions to shipped" do shipment.update_column(:state, "ready") expect { shipment.ship! }.not_to raise_error end end end end context "when order is completed" do after { Spree::Config.set track_inventory_levels: true } before do allow(order).to receive_messages completed?: true allow(order).to receive_messages canceled?: false end context "with inventory tracking" do before { Spree::Config.set track_inventory_levels: true } it "should validate with inventory" do shipment.inventory_units = [create(:inventory_unit)] expect(shipment.valid?).to be true end end context "without inventory tracking" do before { Spree::Config.set track_inventory_levels: false } it "should validate with no inventory" do expect(shipment.valid?).to be true end end end context "#cancel" do it 'cancels the shipment' do allow(shipment.order).to receive(:update_with_updater!) shipment.state = 'pending' expect(shipment).to receive(:after_cancel) shipment.cancel! expect(shipment.state).to eq 'canceled' end it 'restocks the items' do inventory_unit = mock_model(Spree::InventoryUnit, state: "on_hand", line_item: line_item, variant: variant, quantity: 1) allow(shipment).to receive_message_chain(inventory_units: [inventory_unit]) shipment.stock_location = mock_model(Spree::StockLocation) expect(shipment.stock_location).to receive(:restock).with(variant, 1, shipment) shipment.after_cancel end context "with backordered inventory units" do let(:order) { create(:order) } let(:variant) { create(:variant) } let(:other_order) { create(:order) } before do order.contents.add variant order.create_proposed_shipments other_order.contents.add variant other_order.create_proposed_shipments end it "doesn't fill backorders when restocking inventory units" do shipment = order.shipments.first expect(shipment.inventory_units.count).to eq 1 expect(shipment.inventory_units.first).to be_backordered other_shipment = other_order.shipments.first expect(other_shipment.inventory_units.count).to eq 1 expect(other_shipment.inventory_units.first).to be_backordered expect { shipment.cancel! }.not_to change { other_shipment.inventory_units.first.state } end end end context "#resume" do it 'transitions state to ready if the order is ready' do allow(shipment.order).to receive(:update_with_updater!) shipment.state = 'canceled' expect(shipment).to receive(:determine_state).and_return('ready') expect(shipment).to receive(:after_resume) shipment.resume! expect(shipment.state).to eq 'ready' end it 'transitions state to pending if the order is not ready' do allow(shipment.order).to receive(:update_with_updater!) shipment.state = 'canceled' expect(shipment).to receive(:determine_state).and_return('pending') expect(shipment).to receive(:after_resume) shipment.resume! # Shipment is pending because order is already paid expect(shipment.state).to eq 'pending' end it 'unstocks them items' do inventory_unit = mock_model(Spree::InventoryUnit, quantity: 1, line_item: line_item, variant: variant) allow(shipment).to receive_message_chain(inventory_units: [inventory_unit]) shipment.stock_location = mock_model(Spree::StockLocation) expect(shipment.stock_location).to receive(:unstock).with(variant, 1, shipment) shipment.after_resume end end context "#ship" do context "when the shipment is canceled" do let(:shipment_with_inventory_units) { create(:shipment, order: create(:order_with_line_items), state: 'canceled') } let(:subject) { shipment_with_inventory_units.ship! } before do allow(order).to receive(:update_with_updater!) allow(shipment_with_inventory_units).to receive_messages(require_inventory: false, update_order: true) end it 'unstocks them items' do allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) expect(shipment_with_inventory_units.stock_location).to receive(:unstock) subject end end ['ready', 'canceled'].each do |state| context "from #{state}" do before do allow(order).to receive(:update_with_updater!) allow(shipment).to receive_messages(require_inventory: false, update_order: true, state: state) end it "should update shipped_at timestamp" do allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) shipment.ship! expect(shipment.shipped_at).not_to be_nil # Ensure value is persisted shipment.reload expect(shipment.shipped_at).not_to be_nil end it "should send a shipment email" do mail_message = double 'Mail::Message' shipment_id = nil expect(Spree::ShipmentMailer).to receive(:shipped_email) { |*args| shipment_id = args[0] mail_message } expect(mail_message).to receive :deliver_later allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) shipment.ship! expect(shipment_id).to eq(shipment.id) end it "finalizes adjustments" do allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) shipment.adjustments.each do |adjustment| expect(adjustment).to receive(:finalize!) end shipment.ship! end end end end context "#ready" do context 'with Config.auto_capture_on_dispatch == false' do # Regression test for #2040 it "cannot ready a shipment for an order if the order is unpaid" do allow(order).to receive_messages(paid?: false) assert !shipment.can_ready? end end context 'with Config.auto_capture_on_dispatch == true' do before do Spree::Config[:auto_capture_on_dispatch] = true @order = create :completed_order_with_pending_payment @shipment = @order.shipments.first @shipment.cost = @order.ship_total end it "shipments ready for an order if the order is unpaid" do expect(@shipment.ready?).to be true end it "tells the order to process payment in #after_ship" do expect(@shipment).to receive(:process_order_payments) @shipment.ship! end context "order has pending payments" do let(:payment) do payment = @order.payments.first payment.update_attribute :state, 'pending' payment end it "can fully capture an authorized payment" do payment.update_attribute(:amount, @order.total) expect(payment.amount).to eq payment.uncaptured_amount @shipment.ship! expect(payment.reload.uncaptured_amount.to_f).to eq 0 end it "can partially capture an authorized payment" do payment.update_attribute(:amount, @order.total + 50) expect(payment.amount).to eq payment.uncaptured_amount @shipment.ship! expect(payment.captured_amount).to eq @order.total expect(payment.captured_amount).to eq payment.amount expect(payment.order.payments.pending.first.amount).to eq 50 end end end end context "updates cost when selected shipping rate is present" do let(:shipment) { create(:shipment) } before { allow(shipment).to receive_message_chain :selected_shipping_rate, cost: 5 } it "updates shipment totals" do shipment.update_amounts expect(shipment.reload.cost).to eq(5) end it "factors in additional adjustments to adjustment total" do shipment.adjustments.create!( order: order, label: "Additional", amount: 5, included: false, state: "closed" ) shipment.update_amounts expect(shipment.reload.adjustment_total).to eq(5) end it "does not factor in included adjustments to adjustment total" do shipment.adjustments.create!( order: order, label: "Included", amount: 5, included: true, state: "closed" ) shipment.update_amounts expect(shipment.reload.adjustment_total).to eq(0) end end context "changes shipping rate via general update" do let(:order) do Spree::Order.create( payment_total: 100, payment_state: 'paid', total: 100, item_total: 100 ) end let(:shipment) { Spree::Shipment.create order_id: order.id, stock_location: create(:stock_location) } let(:shipping_rate) do Spree::ShippingRate.create shipment_id: shipment.id, cost: 10 end before do shipment.update_attributes_and_order selected_shipping_rate_id: shipping_rate.id end it "updates everything around order shipment total and state" do expect(shipment.cost.to_f).to eq 10 expect(shipment.state).to eq 'pending' expect(shipment.order.total.to_f).to eq 110 expect(shipment.order.payment_state).to eq 'balance_due' end end context "after_save" do context "line item changes" do before do shipment.cost = shipment.cost + 10 end it "triggers adjustment total recalculation" do expect(shipment).to receive(:recalculate_adjustments) shipment.save end it "does not trigger adjustment recalculation if shipment has shipped" do shipment.state = 'shipped' expect(shipment).not_to receive(:recalculate_adjustments) shipment.save end end context "line item does not change" do it "does not trigger adjustment total recalculation" do expect(shipment).not_to receive(:recalculate_adjustments) shipment.save end end end context "currency" do it "returns the order currency" do expect(shipment.currency).to eq(order.currency) end end context "nil costs" do it "sets cost to 0" do shipment = Spree::Shipment.new shipment.valid? expect(shipment.cost).to eq 0 end end context "#tracking_url" do it "uses shipping method to determine url" do expect(shipping_method).to receive(:build_tracking_url).with('1Z12345').and_return(:some_url) shipment.tracking = '1Z12345' expect(shipment.tracking_url).to eq(:some_url) end end context "set up new inventory units" do # let(:line_item) { double( let(:variant) { double("Variant", id: 9) } let(:inventory_units) { double } let(:params) do { variant_id: variant.id, state: 'on_hand', order_id: order.id, line_item_id: line_item.id } end before { allow(shipment).to receive_messages inventory_units: inventory_units } it "associates variant and order" do expect(inventory_units).to receive(:create).with(params) unit = shipment.set_up_inventory('on_hand', variant, order, line_item) end end # Regression test for #3349 context "#destroy" do it "destroys linked shipping_rates" do reflection = Spree::Shipment.reflect_on_association(:shipping_rates) expect(reflection.options[:dependent]).to be(:delete_all) end end # Regression test for #4072 (kinda) # The need for this was discovered in the research for #4702 context "state changes" do before do # Must be stubbed so transition can succeed allow(order).to receive_messages paid?: true end it "are logged to the database" do expect(shipment.state_changes).to be_empty expect(shipment.ready!).to be true expect(shipment.state_changes.count).to eq(1) state_change = shipment.state_changes.first expect(state_change.previous_state).to eq('pending') expect(state_change.next_state).to eq('ready') end end end