diff --git a/app/controllers/admin/standing_orders_controller.rb b/app/controllers/admin/standing_orders_controller.rb index 509073e0df..4b8d5a9b57 100644 --- a/app/controllers/admin/standing_orders_controller.rb +++ b/app/controllers/admin/standing_orders_controller.rb @@ -1,7 +1,7 @@ module Admin class StandingOrdersController < ResourceController before_filter :load_shop, only: [:new] - + before_filter :wrap_sli_attrs, only: [:create] respond_to :json respond_override create: { json: { @@ -10,7 +10,7 @@ module Admin } } def new - @standing_order = StandingOrder.new(shop: @shop) + @standing_order.shop = @shop @customers = Customer.of(@shop) @schedules = Schedule.with_coordinator(@shop) @payment_methods = Spree::PaymentMethod.for_distributor(@shop) @@ -29,5 +29,21 @@ module Admin errors end end + + # Wrap :standing_line_items_attributes in :standing_order root + def wrap_sli_attrs + if params[:standing_line_items].is_a? Array + attributes = params[:standing_line_items].map do |sli| + sli.slice(*StandingLineItem.attribute_names) + end + params[:standing_order][:standing_line_items_attributes] = attributes + end + end + + # Overriding Spree method to load data from params here so that + # we can authorise #create using an object with required attributes + def build_resource + StandingOrder.new(shop_id: params[:standing_order].andand[:shop_id]) + end end end diff --git a/app/models/schedule.rb b/app/models/schedule.rb index d3cc6ca429..f8e933b7b3 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -1,5 +1,6 @@ class Schedule < ActiveRecord::Base has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules' + has_many :coordinators, uniq: true, through: :order_cycles attr_accessible :name, :order_cycle_ids diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index c589a5471f..8b7a5f3fac 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -255,7 +255,10 @@ class AbilityDecorator can [:create], Customer can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) - can [:admin, :new, :create, :indicative_variant], StandingOrder + can [:admin, :new, :indicative_variant], StandingOrder + can [:create], StandingOrder do |standing_order| + user.enterprises.include?(standing_order.shop) + end end def add_relationship_management_abilities(user) diff --git a/app/models/standing_line_item.rb b/app/models/standing_line_item.rb index dd937d9348..6f4372d4cf 100644 --- a/app/models/standing_line_item.rb +++ b/app/models/standing_line_item.rb @@ -1,8 +1,14 @@ class StandingLineItem < ActiveRecord::Base - belongs_to :standing_order + belongs_to :standing_order, inverse_of: :standing_line_items belongs_to :variant, class_name: 'Spree::Variant' validates :standing_order, presence: true validates :variant, presence: true validates :quantity, { presence: true, numericality: { only_integer: true } } + + def available_from?(shop, schedule) + Spree::Variant.joins(exchanges: { order_cycle: :schedules}) + .where(id: variant_id, schedules: { id: schedule}, exchanges: { incoming: false, receiver_id: shop }) + .any? + end end diff --git a/app/models/standing_order.rb b/app/models/standing_order.rb index 7bf2015bbd..8557361710 100644 --- a/app/models/standing_order.rb +++ b/app/models/standing_order.rb @@ -4,19 +4,34 @@ class StandingOrder < ActiveRecord::Base belongs_to :schedule belongs_to :shipping_method, class_name: 'Spree::ShippingMethod' belongs_to :payment_method, class_name: 'Spree::PaymentMethod' - has_many :standing_line_items + has_many :standing_line_items, inverse_of: :standing_order - validates :shop, presence: true - validates :customer, presence: true - validates :schedule, presence: true - validates :shipping_method, presence: true - validates :payment_method, presence: true - validates :begins_at, presence: true + accepts_nested_attributes_for :standing_line_items + + validates_presence_of :shop, :customer, :schedule, :payment_method, :shipping_method, :begins_at validate :ends_at_after_begins_at + validate :standing_line_items_available + validate :check_associations - def ends_at_after_begins_at - if begins_at.present? && ends_at.present? && ends_at <= begins_at - errors.add(:ends_at, "must be after begins at") - end - end + def ends_at_after_begins_at + if begins_at.present? && ends_at.present? && ends_at <= begins_at + errors.add(:ends_at, "must be after begins at") + end + end + + def check_associations + errors[:customer] << "Customer does not belong to the #{shop.name}" unless customer.andand.enterprise == shop + errors[:schedule] << "Schedule is not coordinated by #{shop.name}" unless schedule.andand.coordinators.andand.include? shop + errors[:payment_method] << "Payment Method is not available to #{shop.name}" unless payment_method.andand.distributors.andand.include? shop + errors[:shipping_method] << "Shipping Method is not available to #{shop.name}" unless shipping_method.andand.distributors.andand.include? shop + end + + def standing_line_items_available + standing_line_items.each do |sli| + unless sli.available_from?(shop_id, schedule_id) + name = "#{sli.variant.product.name} - #{sli.variant.full_name}" + errors[:base] << "#{name} is not available from the selected schedule" + end + end + end end diff --git a/app/serializers/api/admin/estimated_variant_serializer.rb b/app/serializers/api/admin/estimated_variant_serializer.rb index 25c1177ecc..ed7b2f3c74 100644 --- a/app/serializers/api/admin/estimated_variant_serializer.rb +++ b/app/serializers/api/admin/estimated_variant_serializer.rb @@ -1,8 +1,12 @@ class Api::Admin::EstimatedVariantSerializer < ActiveModel::Serializer - attributes :id, :product_name, :full_name, :price_with_fees + attributes :variant_id, :description, :price_with_fees - def product_name - object.product.name + def variant_id + object.id + end + + def description + "#{object.product.name} - #{object.full_name}" end def price_with_fees diff --git a/app/views/admin/standing_orders/_items.html.haml b/app/views/admin/standing_orders/_items.html.haml index f44ae80965..698f46bf84 100644 --- a/app/views/admin/standing_orders/_items.html.haml +++ b/app/views/admin/standing_orders/_items.html.haml @@ -14,8 +14,8 @@ %span= t(:total) %th.orders-actions.actions %tbody - %tr{ ng: { repeat: 'item in standingOrder.standing_line_items', class: { even: 'even', odd: 'odd' } } } - %td.description {{ item.product_name }} - {{ item.full_name }} + %tr.item{ ng: { repeat: 'item in standingOrder.standing_line_items', class: { even: 'even', odd: 'odd' } } } + %td.description {{ item.description }} %td.price.align-center {{ item.price_with_fees | currency }} %td.qty %input.qty{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } } diff --git a/lib/open_food_network/nested_attributes_params_wrapper.rb b/lib/open_food_network/nested_attributes_params_wrapper.rb new file mode 100644 index 0000000000..376e8254bc --- /dev/null +++ b/lib/open_food_network/nested_attributes_params_wrapper.rb @@ -0,0 +1,8 @@ +module OpenFoodNetwork + module NestAttributesParamsWrapper + model_klass = controller_path.classify + + nested_attributes_names = StandingOrder.nested_attributes_options.keys.map { |k| k.to_s.concat('_attributes').to_sym } + wrap_parameters include: StandingOrder.attribute_names + nested_attributes_names, format: :json + end +end diff --git a/spec/controllers/admin/standing_orders_controller_spec.rb b/spec/controllers/admin/standing_orders_controller_spec.rb index 2fa3d1a7c9..ed7fa7216d 100644 --- a/spec/controllers/admin/standing_orders_controller_spec.rb +++ b/spec/controllers/admin/standing_orders_controller_spec.rb @@ -28,4 +28,114 @@ describe Admin::StandingOrdersController, type: :controller do expect(assigns(:shipping_methods)).to eq [shipping_method] end end + + describe 'create' do + let!(:user) { create(:user) } + let!(:shop) { create(:distributor_enterprise, owner: user) } + let!(:customer) { create(:customer, enterprise: shop) } + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + let!(:payment_method) { create(:payment_method, distributors: [shop]) } + let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } + let(:params) { { format: :json, standing_order: { shop_id: shop.id } } } + + context 'as an non-manager of the specified shop' do + before do + allow(controller).to receive(:spree_current_user) { create(:user, enterprises: [create(:enterprise)]) } + end + + it 'redirects to unauthorized' do + spree_post :create, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context 'as a manager of the specified shop' do + before do + allow(controller).to receive(:spree_current_user) { user } + end + + context 'when I submit insufficient params' do + it 'returns errors' do + expect{ spree_post :create, params }.to_not change{StandingOrder.count} + json_response = JSON.parse(response.body) + expect(json_response['errors'].keys).to include 'schedule', 'customer', 'payment_method', 'shipping_method', 'begins_at' + end + end + + context 'when I submit params containing ids of inaccessible objects' do + # As 'user' I shouldnt be able to associate a standing_order with any of these. + let(:unmanaged_enterprise) { create(:enterprise) } + let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_enterprise)]) } + let(:unmanaged_customer) { create(:customer, enterprise: unmanaged_enterprise) } + let(:unmanaged_payment_method) { create(:payment_method, distributors: [unmanaged_enterprise]) } + let(:unmanaged_shipping_method) { create(:shipping_method, distributors: [unmanaged_enterprise]) } + + before do + params[:standing_order].merge!({ + schedule_id: unmanaged_schedule.id, + customer_id: unmanaged_customer.id, + payment_method_id: unmanaged_payment_method.id, + shipping_method_id: unmanaged_shipping_method.id, + begins_at: 2.days.ago, + ends_at: 3.weeks.ago + }) + end + + it 'returns errors' do + expect{ spree_post :create, params }.to_not change{StandingOrder.count} + json_response = JSON.parse(response.body) + expect(json_response['errors'].keys).to include 'schedule', 'customer', 'payment_method', 'shipping_method', 'ends_at' + end + end + + context 'when I submit valid and complete params' do + before do + params[:standing_order].merge!({ + schedule_id: schedule.id, + customer_id: customer.id, + payment_method_id: payment_method.id, + shipping_method_id: shipping_method.id, + begins_at: 2.days.ago, + ends_at: 3.months.from_now + }) + end + + it 'creates a standing order' do + expect{ spree_post :create, params }.to change{StandingOrder.count}.by(1) + standing_order = StandingOrder.last + expect(standing_order.schedule).to eq schedule + expect(standing_order.customer).to eq customer + expect(standing_order.payment_method).to eq payment_method + expect(standing_order.shipping_method).to eq shipping_method + end + + context 'with standing_line_items params' do + let(:variant) { create(:variant) } + before { params[:standing_line_items] = [{ quantity: 2, variant_id: variant.id}] } + + context 'where the specified variants are not available from the shop' do + it 'returns an error' do + expect{ spree_post :create, params }.to_not change{StandingOrder.count} + json_response = JSON.parse(response.body) + expect(json_response['errors']['base']).to eq ["#{variant.product.name} - #{variant.full_name} is not available from the selected schedule"] + end + end + + context 'where the specified variants are available from the shop' do + let!(:exchange) { create(:exchange, order_cycle: order_cycle, incoming: false, receiver: shop, variants: [variant])} + + it 'creates standing line items for the standing order' do + expect{ spree_post :create, params }.to change{StandingOrder.count}.by(1) + standing_order = StandingOrder.last + expect(standing_order.standing_line_items.count).to be 1 + standing_line_item = standing_order.standing_line_items.first + expect(standing_line_item.quantity).to be 2 + expect(standing_line_item.variant).to eq variant + end + end + end + end + end + end end diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb index 4914efd79a..0952d489ec 100644 --- a/spec/controllers/spree/admin/variants_controller_spec.rb +++ b/spec/controllers/spree/admin/variants_controller_spec.rb @@ -110,8 +110,7 @@ module Spree json_response = JSON.parse(response.body) expect(json_response['price_with_fees']).to eq 18.5 - expect(json_response['product_name']).to eq variant.product.name - expect(json_response['full_name']).to eq '100g' + expect(json_response['description']).to eq "#{variant.product.name} - 100g" end end end diff --git a/spec/features/admin/standing_orders_spec.rb b/spec/features/admin/standing_orders_spec.rb index 3458010c49..a1af6f9e6a 100644 --- a/spec/features/admin/standing_orders_spec.rb +++ b/spec/features/admin/standing_orders_spec.rb @@ -27,6 +27,17 @@ feature 'Standing Orders' do select2_select payment_method.name, from: 'payment_method_id' select2_select shipping_method.name, from: 'shipping_method_id' + # Adding a product and getting a price estimate + targetted_select2_search product.name, from: '#add_variant_id', dropdown_css: '.select2-drop' + fill_in 'add_quantity', with: 2 + click_link 'Add' + within 'table#standing-line-items tr.item', match: :first do + expect(page).to have_selector 'td.description', text: "#{product.name} - #{variant.full_name}" + expect(page).to have_selector 'td.price', text: "$13.75" + expect(page).to have_input 'quantity', with: "2" + expect(page).to have_selector 'td.total', text: "$27.50" + end + # No date filled out, so error returned expect{ click_button('Save') @@ -42,22 +53,18 @@ feature 'Standing Orders' do expect(page).to have_content 'Saved' }.to change(StandingOrder, :count).by(1) + # Basic properties of standing order are set standing_order = StandingOrder.last expect(standing_order.customer).to eq customer expect(standing_order.schedule).to eq schedule expect(standing_order.payment_method).to eq payment_method expect(standing_order.shipping_method).to eq shipping_method - end - it "I can select a product to the list and get a price estimate" do - visit new_admin_standing_order_path(shop_id: shop.id) - - select2_select schedule.name, from: 'schedule_id' - targetted_select2_search product.name, from: '#add_variant_id', dropdown_css: '.select2-drop' - click_link 'Add' - - expect(page).to have_selector 'table#standing-line-items td.description', text: "#{product.name} - #{variant.full_name}" - expect(page).to have_selector 'table#standing-line-items td.price', text: "$13.75" + # Standing Line Items are created + expect(standing_order.standing_line_items.count).to eq 1 + standing_line_item = standing_order.standing_line_items.first + expect(standing_line_item.variant).to eq variant + expect(standing_line_item.quantity).to eq 2 end end end