mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
StandingLineItems added to StandingOrder are saved
Adding some additional validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' } }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user