Files
openfoodnetwork/spec/controllers/api/v0/shipments_controller_spec.rb
Maikel Linke dcb6f4676d Remove all unnecessary spec_helper require statements
The `.rspec` file is doing this for us.
2026-01-21 12:35:34 +11:00

435 lines
15 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Api::V0::ShipmentsController do
render_views
let!(:shipment) { create(:shipment) }
let(:attributes) { %w[id tracking number cost shipped_at order_id] }
let(:current_api_user) { build(:user) }
before do
allow(controller).to receive(:spree_current_user) { current_api_user }
end
context "as a non-admin" do
it "cannot make a shipment ready" do
api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param
assert_unauthorized!
end
it "cannot make a shipment shipped" do
api_put :ship, order_id: shipment.order.to_param, id: shipment.to_param
assert_unauthorized!
end
end
context "as an admin" do
let(:current_api_user) { build(:admin_user) }
let!(:order) { shipment.order }
let(:order_ship_address) { create(:address) }
let!(:variant) { create(:variant) }
let(:params) do
{ quantity: 2,
variant_id: variant.to_param,
order_id: order.number,
format: :json }
end
let(:error_message) { "broken shipments creation" }
before do
order.update_attribute :ship_address_id, order_ship_address.id
order.update_attribute :distributor, variant.supplier
shipment.shipping_method.distributors << variant.supplier
end
context '#create' do
it 'creates a shipment if order does not have a shipment' do
order.shipment.destroy
order.reload
spree_post :create, params
expect_valid_response
expect(order.shipment.reload.inventory_units.size).to eq 2
expect(order.reload.line_items.first.variant.price).to eq(variant.price)
end
it 'updates and returns exiting shipment, if order already has a shipment' do
original_shipment_id = order.shipment.id
spree_post :create, params
expect(json_response["id"]).to eq(original_shipment_id)
expect_valid_response
expect(order.shipment.reload.inventory_units.size).to eq 2
expect(order.reload.line_items.first.variant.price).to eq(variant.price)
end
it 'updates existing shipment with variant override if an VO is sent', feature: :inventory do
hub = create(:distributor_enterprise)
order.update_attribute(:distributor, hub)
shipment.shipping_method.distributors << hub
variant_override = create(:variant_override, hub:, variant:)
spree_post :create, params
expect_valid_response
expect(order.shipment.reload.inventory_units.size).to eq 2
expect(order.reload.line_items.first.price).to eq(variant_override.price)
end
it 'returns error code when adding to order contents fails' do
make_order_contents_fail
spree_post :create, params
expect_error_response
end
it "applies any enterprise fees that are present" do
order_cycle = create(:simple_order_cycle,
coordinator: order.distributor,
coordinator_fees: [create(:enterprise_fee, amount: 20)],
distributors: [order.distributor],
variants: [variant])
order.update!(order_cycle_id: order_cycle.id)
spree_post :create, params
expect(order.line_item_adjustments.where(originator_type: "EnterpriseFee")).to be_present
end
end
it "can make a shipment ready" do
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param
expect(json_response.keys).to include(*attributes)
expect(json_response["state"]).to eq("ready")
expect(shipment.reload.state).to eq("ready")
end
it "checks if shipment is ready already" do
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
shipment.ready!
api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param
expect(json_response.keys).to include(*attributes)
expect(json_response["state"]).to eq("ready")
expect(shipment.reload.state).to eq("ready")
end
it "cannot make a shipment ready if the order is unpaid" do
allow_any_instance_of(Spree::Order).to receive_messages(paid?: false)
api_put :ready, order_id: shipment.order.to_param, id: shipment.to_param
expect(json_response["error"]).to eq("Cannot ready shipment.")
expect(response).to have_http_status(:unprocessable_entity)
end
describe "#add and #remove" do
let(:order) { create :completed_order_with_totals }
let(:line_item) { order.line_items.first }
let(:existing_variant) { line_item.variant }
let(:new_variant) { create(:variant) }
let(:params) {
{
quantity: 2,
order_id: order.to_param,
id: order.shipments.first.to_param
}
}
before do
line_item.update!(quantity: 3)
end
context 'for completed shipments' do
it 'adds a variant to a shipment' do
expect {
api_put :add, params.merge(variant_id: new_variant.to_param)
expect(response).to have_http_status(:ok)
}.to change { inventory_units_for(new_variant).size }.by(2)
end
it 'adjusts stock when adding a variant' do
expect {
api_put :add, params.merge(variant_id: new_variant.to_param)
}.to change { new_variant.reload.on_hand }.by(-2)
end
it 'removes a variant from a shipment' do
expect {
api_put :remove, params.merge(variant_id: existing_variant.to_param)
expect(response).to have_http_status(:ok)
}.to change { inventory_units_for(existing_variant).size }.by(-2)
end
it 'adjusts stock when removing a variant' do
expect {
api_put :remove, params.merge(variant_id: existing_variant.to_param)
}.to change { existing_variant.reload.on_hand }.by(2)
end
it 'does not adjust stock when removing a variant' do
expect {
api_put :remove, params.merge(variant_id: existing_variant.to_param,
restock_item: 'false')
}.to change { existing_variant.reload.on_hand }.by(0)
end
end
context "for canceled orders" do
before do
expect(order.cancel).to eq true
end
it "doesn't adjust stock when adding a variant" do
expect {
api_put :add, params.merge(variant_id: existing_variant.to_param)
expect(response).to have_http_status(:unprocessable_entity)
}.not_to change { existing_variant.reload.on_hand }
end
it "doesn't adjust stock when removing a variant" do
expect {
api_put :remove, params.merge(variant_id: existing_variant.to_param)
expect(response).to have_http_status(:unprocessable_entity)
}.not_to change { existing_variant.reload.on_hand }
end
end
context "with shipping fees" do
let!(:distributor) { create(:distributor_enterprise) }
let(:fee_amount) { 10 }
let!(:shipping_method_with_fee) {
create(:shipping_method_with, :shipping_fee, distributors: [distributor],
shipping_fee: fee_amount)
}
let!(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
let!(:order) {
create(:completed_order_with_totals, order_cycle:, distributor:)
}
let(:shipping_fee) { order.reload.shipment.adjustments.first }
before do
order.shipments.first.shipping_methods = [shipping_method_with_fee]
order.select_shipping_method(shipping_method_with_fee.id)
order.update_order!
end
context "adding item to a shipment" do
it "updates the shipping fee" do
expect {
api_put :add, params.merge(variant_id: new_variant.to_param)
}.to change { order.reload.shipment.adjustments.first.amount }.by(20)
end
end
context "removing item from a shipment" do
it "updates the shipping fee" do
expect {
api_put :remove, params.merge(variant_id: existing_variant.to_param)
}.to change { order.reload.shipment.adjustments.first.amount }.by(-20)
end
end
end
end
describe "#update" do
let!(:distributor) { create(:distributor_enterprise) }
let!(:shipping_method1) {
create(:shipping_method_with, :flat_rate, distributors: [distributor], amount: 10)
}
let!(:shipping_method2) {
create(:shipping_method_with, :flat_rate, distributors: [distributor], amount: 20)
}
let!(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
let!(:order) {
create(:completed_order_with_totals, order_cycle:, distributor:)
}
let(:new_shipping_rate) {
order.shipment.shipping_rates.select{ |sr| sr.shipping_method == shipping_method2 }.first
}
let(:params) {
{
id: order.shipment.number,
order_id: order.number,
shipment: {
selected_shipping_rate_id: new_shipping_rate.id
}
}
}
before do
order.shipments.first.shipping_methods = [shipping_method1, shipping_method2]
order.shipments.each(&:refresh_rates)
order.select_shipping_method(shipping_method1.id)
order.update_order!
order.update_columns(
payment_total: 60,
payment_state: "paid"
)
end
context "when an order has multiple shipping methods available which could be chosen" do
context "changing the selected shipping method" do
it "updates the order's totals and states" do
expect(order.shipment.shipping_method).to eq shipping_method1
expect(order.shipment.cost).to eq 10
expect(order.total).to eq 60 # item total is 50, shipping cost is 10
expect(order.payment_state).to eq "paid" # order is fully paid for
api_put :update, params
expect(response).to have_http_status :ok
order.reload
expect(order.shipment.shipping_method).to eq shipping_method2
expect(order.shipment.cost).to eq 20
expect(order.total).to eq 70 # item total is 50, shipping cost is 20
expect(order.payment_state).to eq "balance_due" # total changed, payment is due
end
it "updates closed adjustments" do
expect {
api_put :update, params
expect(response).to have_http_status :ok
}.to change { order.reload.shipment.fee_adjustment.amount }
end
end
end
end
context "#ship" do
before do
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
# For the shipment notification email
Spree::Config[:mails_from] = "ofn@example.com"
shipment.update!(shipment.order)
expect(shipment.state).to eq("ready")
allow_any_instance_of(Spree::ShippingRate).to receive_messages(cost: 5)
end
it "can transition a shipment from ready to ship" do
shipment.reload
api_put :ship, order_id: shipment.order.to_param,
id: shipment.to_param,
shipment: { tracking: "123123" }
expect(json_response.keys).to include(*attributes)
expect(json_response["state"]).to eq("shipped")
end
end
context 'for a completed order with shipment' do
let(:order) { create :completed_order_with_totals }
before { params[:id] = order.shipments.first.to_param }
context '#add' do
it 'adds a variant to the shipment' do
spree_put :add, params
expect_valid_response
expect(inventory_units_for(variant).size).to eq 2
end
it 'returns error code when adding to order contents fails' do
make_order_contents_fail
spree_put :add, params
expect_error_response
end
it 'adds a variant override to the shipment', feature: :inventory do
hub = create(:distributor_enterprise)
order.update_attribute(:distributor, hub)
variant_override = create(:variant_override, hub:, variant:)
spree_put :add, params
expect_valid_response
expect(inventory_units_for(variant).size).to eq 2
expect(order.reload.line_items.last.price).to eq(variant_override.price)
end
context "when line items have fees" do
let(:fee_order) {
instance_double(Spree::Order, number: "123", distributor: variant.supplier)
}
let(:contents) { instance_double(Spree::OrderContents) }
let(:fee_order_shipment) {
instance_double(Spree::Shipment)
}
before do
allow(Spree::Order).to receive(:find_by!) { fee_order }
allow(controller).to receive(:refuse_changing_cancelled_orders)
allow(fee_order).to receive(:contents) { contents }
allow(contents).to receive_messages(add: {}, remove: {})
allow(fee_order).to receive_message_chain(:shipments, :find_by!) { fee_order_shipment }
allow(fee_order_shipment).to receive_messages(update: nil, reload: nil, persisted?: nil)
allow(fee_order).to receive(:recreate_all_fees!)
end
it "recalculates fees for the line item" do
params[:order_id] = fee_order.number
spree_put :add, params
expect(fee_order).to have_received(:recreate_all_fees!)
end
it "recalculates fees for the line item when qty is decreased" do
params[:order_id] = fee_order.number
spree_put :remove, params
expect(fee_order).to have_received(:recreate_all_fees!)
end
end
end
context '#remove' do
before do
params[:variant_id] = order.line_items.first.variant.to_param
params[:quantity] = 1
end
it 'removes a variant from the shipment' do
spree_put :remove, params
expect_valid_response
expect(inventory_units_for(variant).size).to eq 0
end
it 'returns error code when removing from order contents fails' do
make_order_contents_fail
spree_put :remove, params
expect_error_response
end
end
end
def inventory_units_for(variant)
order.shipment.reload.inventory_units.select { |unit| unit['variant_id'] == variant.id }
end
def expect_valid_response
expect(response).to have_http_status :ok
expect(json_response.keys).to include(*attributes)
end
def make_order_contents_fail
expect(Spree::Order).to receive(:find_by!).with({ number: order.number }) { order }
expect(order).to receive(:contents) { raise error_message }
end
def expect_error_response
expect(response).to have_http_status :unprocessable_entity
expect(json_response["exception"]).to eq error_message
end
end
end