Moving action for estimating standing_line_item prices into StandingLineItemController#build

This commit is contained in:
Rob Harrington
2016-09-16 17:06:26 +10:00
parent 71b84d490e
commit 946046e53b
21 changed files with 248 additions and 153 deletions

View File

@@ -17,7 +17,7 @@ angular.module("admin.standingOrders").controller "StandingOrderController", ($s
$scope.estimatedSubtotal = ->
$scope.standingOrder.standing_line_items.reduce (subtotal, item) ->
item.price_with_fees * item.quantity
item.price_estimate * item.quantity
, 0
$scope.estimatedTotal = ->

View File

@@ -1,4 +1,4 @@
angular.module("admin.standingOrders").factory "StandingOrder", ($injector, $http, StatusMessage, StandingOrderResource) ->
angular.module("admin.standingOrders").factory "StandingOrder", ($injector, $http, StatusMessage, InfoDialog, StandingOrderResource) ->
new class StandingOrder
standingOrder: new StandingOrderResource()
errors: {}
@@ -10,13 +10,11 @@ angular.module("admin.standingOrders").factory "StandingOrder", ($injector, $htt
buildItem: (item) ->
return false unless item.variant_id > 0
return false unless item.quantity > 0
estimate_query = "/admin/variants/#{item.variant_id}/price_estimate?"
estimate_query += "shop_id=#{@standingOrder.shop_id};schedule_id=#{@standingOrder.schedule_id}"
$http.get(estimate_query).then (response) =>
angular.extend(response.data, item) # Add variant_id and qty
data = angular.extend({}, item, { shop_id: @standingOrder.shop_id, schedule_id: @standingOrder.schedule_id })
$http.post("/admin/standing_line_items/build", data).then (response) =>
@standingOrder.standing_line_items.push response.data
, (response) =>
alert(response.data.errors)
InfoDialog.open 'error', response.data.errors[0]
save: ->
StatusMessage.display 'progress', 'Saving...'

View File

@@ -0,0 +1,39 @@
require 'open_food_network/permissions'
require 'open_food_network/order_cycle_permissions'
module Admin
class StandingLineItemsController < ResourceController
before_filter :load_build_context, only: [:build]
respond_to :json
def build
return render json: { errors: ['Unauthorised'] }, status: :unauthorized unless @shop
if @variant
@standing_line_item.assign_attributes(params[:standing_line_item])
fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(@shop, @order_cycle) if @order_cycle
OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant)
render json: @standing_line_item, serializer: Api::Admin::StandingLineItemSerializer, fee_calculator: fee_calculator
else
render json: { errors: ["#{@shop.name} is not permitted to sell the selected product"] }, status: :unprocessable_entity
end
end
private
def permissions
OpenFoodNetwork::Permissions.new(spree_current_user)
end
def load_build_context
@shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id])
@schedule = permissions.editable_schedules.find_by_id(params[:schedule_id])
@order_cycle = @schedule.andand.current_or_next_order_cycle
@variant = Spree::Variant.stockable_by(@shop).find_by_id(params[:standing_line_item][:variant_id])
end
def new_actions
[:new, :create, :build] # Added build
end
end
end

View File

@@ -5,7 +5,11 @@ module Admin
respond_to :json
respond_override create: { json: {
success: lambda { render_as_json @standing_order },
success: lambda {
shop, next_oc = @standing_order.shop, @standing_order.schedule.current_or_next_order_cycle
fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc) if shop && next_oc
render_as_json @standing_order, fee_calculator: fee_calculator
},
failure: lambda { render json: { errors: json_errors }, status: :unprocessable_entity }
} }

View File

@@ -1,16 +1,16 @@
require 'open_food_network/permissions'
Spree::Admin::VariantsController.class_eval do
helper 'spree/products'
before_filter :load_price_estimate_context, only: [:price_estimate]
respond_to :json
def search
search_params = { :product_name_cont => params[:q], :sku_cont => params[:q] }
@variants = Spree::Variant.where(is_master: false).ransack(search_params.merge(:m => 'or')).result
if params[:schedule_id].present?
schedule = Schedule.find params[:schedule_id]
@variants = @variants.in_schedule(schedule)
end
if params[:order_cycle_id].present?
order_cycle = OrderCycle.find params[:order_cycle_id]
@variants = @variants.in_order_cycle(order_cycle)
@@ -26,16 +26,6 @@ Spree::Admin::VariantsController.class_eval do
end
end
def price_estimate
if @shop && @schedule && @order_cycle
fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(@shop, @order_cycle)
OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant)
render json: @variant, serializer: Api::Admin::EstimatedVariantSerializer, fee_calculator: fee_calculator
else
render json: { errors: ["Unauthorized"], status: :unprocessable_entity }
end
end
def destroy
@variant = Spree::Variant.find(params[:id])
@variant.delete # This line changed, as well as removal of following conditional
@@ -55,22 +45,4 @@ Spree::Admin::VariantsController.class_eval do
option_values.andand.each_value {|id| @object.option_values << OptionValue.find(id)}
@object.save
end
private
# Allows us to use a variant_id only to look up a price estimate
def parent_data
return super unless action == :price_estimate
nil
end
def permissions
OpenFoodNetwork::Permissions.new(spree_current_user)
end
def load_price_estimate_context
@shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id])
@schedule = permissions.editable_schedules.find_by_id(params[:schedule_id])
@order_cycle = @schedule.andand.current_or_next_order_cycle
end
end

View File

@@ -9,6 +9,6 @@ class Schedule < ActiveRecord::Base
scope :with_coordinator, lambda { |enterprise| joins(:order_cycles).where('coordinator_id = ?', enterprise.id).select('DISTINCT schedules.*') }
def current_or_next_order_cycle
order_cycles.order('orders_close_at ASC').first
order_cycles.where('orders_close_at > (?)', Time.now).order('orders_close_at ASC').first
end
end

View File

@@ -137,9 +137,6 @@ class AbilityDecorator
end
can [:create], Spree::Variant
can [:price_estimate], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).visible_products.include? variant.product
end
can [:admin, :index, :read, :edit, :update, :search, :delete, :destroy], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier
end
@@ -255,10 +252,11 @@ class AbilityDecorator
can [:create], Customer
can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
can [:admin, :new, :indicative_variant], StandingOrder
can [:admin, :new], StandingOrder
can [:create], StandingOrder do |standing_order|
user.enterprises.include?(standing_order.shop)
end
can [:admin, :build], StandingLineItem
end
def add_relationship_management_abilities(user)

View File

@@ -117,6 +117,13 @@ Spree::Product.class_eval do
end
}
scope :stockable_by, lambda { |enterprise|
return where('1=0') unless enterprise.present?
permitted_producer_ids = EnterpriseRelationship.joins(:parent).permitting(enterprise)
.with_permission(:add_to_order_cycle).where(enterprises: { is_primary_producer: true }).pluck(:parent_id)
return where('spree_products.supplier_id IN (?)', [enterprise.id] | permitted_producer_ids)
}
# -- Methods

View File

@@ -42,6 +42,13 @@ Spree::Variant.class_eval do
select('DISTINCT spree_variants.*')
}
scope :in_schedule, lambda { |schedule|
joins(exchanges: { order_cycle: :schedule}).
merge(Exchange.outgoing).
where(schedules: { id: schedule}).
select('DISTINCT spree_variants.*')
}
scope :for_distribution, lambda { |order_cycle, distributor|
where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor))
}
@@ -58,6 +65,11 @@ Spree::Variant.class_eval do
localize_number :price, :cost_price, :weight
scope :stockable_by, lambda { |enterprise|
return where("1=0") unless enterprise.present?
joins(:product).where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
}
# Define sope as class method to allow chaining with other scopes filtering id.
# In Rails 3, merging two scopes on the same column will consider only the last scope.
def self.in_distributor(distributor)

View File

@@ -1,15 +0,0 @@
class Api::Admin::EstimatedVariantSerializer < ActiveModel::Serializer
attributes :variant_id, :description, :price_with_fees
def variant_id
object.id
end
def description
"#{object.product.name} - #{object.full_name}"
end
def price_with_fees
(object.price + options[:fee_calculator].indexed_fees_for(object)).to_f
end
end

View File

@@ -0,0 +1,15 @@
class Api::Admin::StandingLineItemSerializer < ActiveModel::Serializer
attributes :id, :variant_id, :quantity, :description, :price_estimate
def description
"#{object.variant.product.name} - #{object.variant.full_name}"
end
def price_estimate
if options[:fee_calculator]
(object.variant.price + options[:fee_calculator].indexed_fees_for(object.variant)).to_f
else
"?"
end
end
end

View File

@@ -1,7 +1,7 @@
class Api::Admin::StandingOrderSerializer < ActiveModel::Serializer
attributes :id, :shop_id, :customer_id, :schedule_id, :payment_method_id, :shipping_method_id, :begins_at, :ends_at
has_many :standing_line_items
has_many :standing_line_items, serializer: Api::Admin::StandingLineItemSerializer
def begins_at
object.begins_at.andand.strftime('%F')

View File

@@ -16,10 +16,10 @@
%tbody
%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.price.align-center {{ item.price_estimate | currency }}
%td.qty
%input.qty{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } }
%td.total.align-center {{ (item.price_with_fees * item.quantity) | currency }}
%td.total.align-center {{ (item.price_estimate * item.quantity) | currency }}
%td.actions
%a.delete-resource.icon_link.with-tip.icon-trash.no-text{ data: { confirm: "Are you sure?" }, :href => "javascript:void(0)" }
%tbody#subtotal.no-border-top{"data-hook" => "admin_order_form_subtotal"}

View File

@@ -177,8 +177,10 @@ Openfoodnetwork::Application.routes.draw do
resources :schedules, only: [:index, :create, :update, :destroy], format: :json
resources :standing_orders, only: [:new, :create] do
get :indicative_variant, on: :collection, format: :json
resources :standing_orders, only: [:new, :create]
resources :standing_line_items, only: [], format: :json do
post :build, on: :collection
end
end
@@ -292,10 +294,6 @@ Spree::Core::Engine.routes.prepend do
get :print_ticket, on: :member
get :managed, on: :collection
end
resources :variants, only: [], format: :json do
get :price_estimate, on: :member
end
end
resources :orders do

View File

@@ -118,7 +118,10 @@ module OpenFoodNetwork
return Spree::Variant.where("1=0") unless @order_cycle
if user_manages_coordinator_or(hub)
# TODO: Use variants_stockable_by(hub) for this?
# Any variants produced by the coordinator, for outgoing exchanges with itself
# TODO: isn't this completely redundant given the assignment of hub_variants below?
coordinator_variants = []
if hub == @coordinator
coordinator_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', @coordinator)

View File

@@ -0,0 +1,98 @@
require 'spec_helper'
describe Admin::StandingLineItemsController, type: :controller do
include AuthenticationWorkflow
describe "build" do
let(:user) { create(:user) }
let!(:shop) { create(:enterprise, owner: user) }
let(:unmanaged_shop) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle])}
let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_shop)]) }
context "json" do
let(:params) { { format: :json, standing_line_item: { quantity: 2, variant_id: variant.id } } }
context 'as an enterprise user' do
before { allow(controller).to receive(:spree_current_user) { user } }
context "but no shop_id is provided" do
it "returns an error" do
spree_post :build, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorised']
end
end
context "and an unmanaged shop_id is provided" do
before { params.merge!({ shop_id: unmanaged_shop.id }) }
it "returns an error" do
spree_post :build, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorised']
end
end
context "where a managed shop_id is provided" do
before { params.merge!({ shop_id: shop.id }) }
context "but the shop doesn't have permission to sell product in question" do
it "returns an error" do
spree_post :build, params
json_response = JSON.parse(response.body)
expect(json_response['errors']).to eq ["#{shop.name} is not permitted to sell the selected product"]
end
end
context "and the shop has permission to sell the product in question" do
before do
product.update_attribute(:supplier_id, shop.id)
end
context "but no schedule_id is provided" do
it "returns a serialized standing line item without a price estimate" do
spree_post :build, params
json_response = JSON.parse(response.body)
expect(json_response['price_estimate']).to eq '?'
expect(json_response['quantity']).to eq 2
expect(json_response['description']).to eq "#{variant.product.name} - 100g"
end
end
context "but an unmanaged schedule_id is provided" do
before { params.merge!({ schedule_id: unmanaged_schedule.id }) }
it "returns a serialized standing line item without a price estimate" do
spree_post :build, params
json_response = JSON.parse(response.body)
expect(json_response['price_estimate']).to eq '?'
expect(json_response['quantity']).to eq 2
expect(json_response['description']).to eq "#{variant.product.name} - 100g"
end
end
context "and a managed schedule_id is provided" do
before { params.merge!({ schedule_id: schedule.id }) }
it "returns a serialized standing line item with a price estimate" do
spree_post :build, params
json_response = JSON.parse(response.body)
expect(json_response['price_estimate']).to eq 18.5
expect(json_response['quantity']).to eq 2
expect(json_response['description']).to eq "#{variant.product.name} - 100g"
end
end
end
end
end
end
end
end

View File

@@ -35,88 +35,6 @@ module Spree
assigns(:variants).should match_array [v1, v2]
end
end
describe "price_estimate" do
let(:user) { create(:user) }
let!(:enterprise) { create(:enterprise, owner: user) }
let(:unmanaged_enterprise) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: enterprise, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: enterprise, receiver: enterprise, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle])}
let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_enterprise)]) }
context "json" do
let(:params) { { format: :json, id: variant.id } }
context 'as an enterprise user' do
before { allow(controller).to receive(:spree_current_user) { user } }
context "where I don't have access to the product in question" do
it "redirects to unauthorized" do
spree_get :price_estimate, params
expect(response).to redirect_to spree.unauthorized_path
end
end
context "where I have access to the product in question" do
before do
product.update_attribute(:supplier_id, enterprise.id)
end
context "but no shop_id is provided" do
before { params.merge!({ schedule_id: schedule.id }) }
it "returns an error" do
spree_get :price_estimate, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorized']
end
end
context "and an unmanaged shop_id is provided" do
before { params.merge!({ shop_id: unmanaged_enterprise.id, schedule_id: schedule.id }) }
it "returns an error" do
spree_get :price_estimate, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorized']
end
end
context "where no schedule_id is provided" do
before { params.merge!({ shop_id: enterprise.id }) }
it "returns an error" do
spree_get :price_estimate, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorized']
end
end
context "and an unmanaged schedule_id is provided" do
before { params.merge!({ shop_id: enterprise.id, schedule_id: unmanaged_schedule.id }) }
it "returns an error" do
spree_get :price_estimate, params
expect(JSON.parse(response.body)['errors']).to eq ['Unauthorized']
end
end
context "where a managed shop_id and schedule_id are provided" do
before { params.merge!({ shop_id: enterprise.id, schedule_id: schedule.id }) }
it "returns a price estimate for the variant" do
spree_get :price_estimate, params
json_response = JSON.parse(response.body)
expect(json_response['price_with_fees']).to eq 18.5
expect(json_response['description']).to eq "#{variant.product.name} - 100g"
end
end
end
end
end
end
end
end
end

View File

@@ -141,7 +141,7 @@ FactoryGirl.define do
factory :standing_order, :class => StandingOrder do
shop { FactoryGirl.create :enterprise }
schedule
customer
customer { create(:customer, enterprise: shop) }
payment_method
shipping_method
begins_at { 1.month.ago }

View File

@@ -53,6 +53,14 @@ feature 'Standing Orders' do
expect(page).to have_content 'Saved'
}.to change(StandingOrder, :count).by(1)
# Prices are shown
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
# Basic properties of standing order are set
standing_order = StandingOrder.last
expect(standing_order.customer).to eq customer

View File

@@ -388,6 +388,26 @@ module Spree
expect(products).to_not include new_variant.product, hidden_variant.product
end
end
describe 'stockable_by' do
let(:shop) { create(:distributor_enterprise) }
let(:add_to_oc_producer) { create(:supplier_enterprise) }
let(:other_producer) { create(:supplier_enterprise) }
let!(:p1) { create(:simple_product, supplier: shop ) }
let!(:p2) { create(:simple_product, supplier: add_to_oc_producer ) }
let!(:p3) { create(:simple_product, supplier: other_producer ) }
before do
create(:enterprise_relationship, parent: add_to_oc_producer, child: shop, permissions_list: [:add_to_order_cycle])
create(:enterprise_relationship, parent: other_producer, child: shop, permissions_list: [:manage_products])
end
it 'shows products produced by the enterprise and any producers granting P-OC' do
stockable_products = Spree::Product.stockable_by(shop)
expect(stockable_products).to include p1, p2
expect(stockable_products).to_not include p3
end
end
end
describe "finders" do

View File

@@ -165,6 +165,26 @@ module Spree
end
end
end
describe 'stockable_by' do
let(:shop) { create(:distributor_enterprise) }
let(:add_to_oc_producer) { create(:supplier_enterprise) }
let(:other_producer) { create(:supplier_enterprise) }
let!(:v1) { create(:variant, product: create(:simple_product, supplier: shop ) ) }
let!(:v2) { create(:variant, product: create(:simple_product, supplier: add_to_oc_producer ) ) }
let!(:v3) { create(:variant, product: create(:simple_product, supplier: other_producer ) ) }
before do
create(:enterprise_relationship, parent: add_to_oc_producer, child: shop, permissions_list: [:add_to_order_cycle])
create(:enterprise_relationship, parent: other_producer, child: shop, permissions_list: [:manage_products])
end
it 'shows variants produced by the enterprise and any producers granting P-OC' do
stockable_variants = Spree::Variant.stockable_by(shop)
expect(stockable_variants).to include v1, v2
expect(stockable_variants).to_not include v3
end
end
end
describe "callbacks" do