StandingLineItems added to StandingOrder are saved

Adding some additional validation
This commit is contained in:
Rob Harrington
2016-09-15 15:59:23 +10:00
parent 415955e7b3
commit e661430cc9
11 changed files with 202 additions and 33 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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' } }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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