Add new 'Checkout options' step to the edit order cycle form so people can attach shipping methods

This commit is contained in:
Cillian O'Ruanaidh
2022-06-08 21:15:38 +01:00
committed by Filipe
parent 1e817af5aa
commit 855ec1a708
16 changed files with 499 additions and 8 deletions

View File

@@ -2,10 +2,10 @@
module Admin
class OrderCyclesController < Admin::ResourceController
include OrderCyclesHelper
include ::OrderCyclesHelper
include PaperTrailLogging
prepend_before_action :set_order_cycle_id, only: [:incoming, :outgoing]
prepend_before_action :set_order_cycle_id, only: [:incoming, :outgoing, :checkout_options]
before_action :load_data_for_index, only: :index
before_action :require_coordinator, only: :new
before_action :remove_protected_attrs, only: [:update]
@@ -67,10 +67,12 @@ module Admin
update_nil_subscription_line_items_price_estimate(@order_cycle)
respond_to do |format|
flash[:notice] = I18n.t(:order_cycles_update_notice) if params[:reloading] == '1'
format.html { redirect_back(fallback_location: root_path) }
format.html { redirect_to_after_update_path }
format.json { render json: { success: true } }
end
else
elsif request.format.html?
render :checkout_options
elsif request.format.json?
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
end
end
@@ -190,6 +192,16 @@ module Admin
end
end
def redirect_to_after_update_path
if params[:context] == "checkout_options" && params[:save]
redirect_to main_app.admin_order_cycle_checkout_options_path(@order_cycle)
elsif params[:context] == "checkout_options" && params[:save_and_back_to_list]
redirect_to main_app.admin_order_cycles_path
else
redirect_back(fallback_location: root_path)
end
end
def require_coordinator
@order_cycle.coordinator =
permitted_coordinating_enterprises_for(@order_cycle).find_by(id: params[:coordinator_id])

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module Admin
module OrderCyclesHelper
def order_cycle_shared_payment_methods(order_cycle)
order_cycle.attachable_payment_methods.select do |payment_method|
(payment_method.distributor_ids & order_cycle.distributor_ids).many?
end
end
def order_cycle_shared_shipping_methods(order_cycle)
order_cycle.attachable_shipping_methods.select do |shipping_method|
(shipping_method.distributor_ids & order_cycle.distributor_ids).many?
end
end
end
end

View File

@@ -156,6 +156,23 @@ class OrderCycle < ApplicationRecord
]
end
def attachable_payment_methods
Spree::PaymentMethod.available(:both).
joins("INNER JOIN distributors_payment_methods
ON payment_method_id = spree_payment_methods.id").
where("distributor_id IN (?)", distributor_ids).
distinct
end
def attachable_shipping_methods
return Spree::ShippingMethod.none if simple? || !shipping_methods_customisable?
Spree::ShippingMethod.frontend.
joins(:distributor_shipping_methods).
where("distributor_id IN (?)", distributor_ids).
distinct
end
def clone!
oc = dup
oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name)

View File

@@ -243,7 +243,9 @@ module Spree
end
def add_order_cycle_management_abilities(user)
can [:admin, :index, :read, :edit, :update, :incoming, :outgoing], OrderCycle do |order_cycle|
can [
:admin, :index, :read, :edit, :update, :incoming, :outgoing, :checkout_options
], OrderCycle do |order_cycle|
OrderCycle.visible_by(user).include? order_cycle
end
can [:admin, :index, :create], Schedule

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
class OrderAvailableShippingMethods < Struct.new(:order, :customer)
delegate :distributor,
:order_cycle,
to: :order
def to_a
return [] if distributor.blank?
shipping_methods = shipping_methods_before_tag_rules_applied
applicator = OpenFoodNetwork::TagRuleApplicator.new(distributor,
"FilterShippingMethods", customer&.tag_list)
applicator.filter!(shipping_methods)
shipping_methods.uniq
end
private
def shipping_methods_before_tag_rules_applied
if order_cycle.nil? || order_cycle.simple?
distributor.shipping_methods
else
distributor.shipping_methods.where(id: order_cycle.shipping_methods.select(:id))
end.frontend.to_a
end
end

View File

@@ -17,7 +17,7 @@ module PermittedAttributes
:name, :orders_open_at, :orders_close_at, :coordinator_id,
:preferred_product_selection_from_coordinator_inventory_only,
:automatic_notifications,
{ schedule_ids: [], coordinator_fee_ids: [] }
{ schedule_ids: [], shipping_method_ids: [], coordinator_fee_ids: [] }
]
end

View File

@@ -6,6 +6,8 @@
= t("admin.order_cycles.wizard_progress.incoming")
%li
= t("admin.order_cycles.wizard_progress.outgoing")
%li
= t("admin.order_cycles.wizard_progress.checkout_options")
- else
%li{ class: "#{'current' if action_name == 'edit'}" }
%a{ href: main_app.edit_admin_order_cycle_path(@order_cycle) }
@@ -16,3 +18,6 @@
%li{ class: "#{'current' if action_name == 'outgoing'}" }
%a{ href: main_app.admin_order_cycle_outgoing_path(@order_cycle) }
= t("admin.order_cycles.wizard_progress.outgoing")
%li{ class: "#{'current' if action_name == 'checkout_options'}" }
%a{ href: main_app.admin_order_cycle_checkout_options_path(@order_cycle) }
= t("admin.order_cycles.wizard_progress.checkout_options")

View File

@@ -0,0 +1,83 @@
= render partial: "/admin/order_cycles/order_cycle_top_buttons"
- content_for :page_title do
= t :edit_order_cycle
- shared_payment_methods = order_cycle_shared_payment_methods(@order_cycle)
- shared_shipping_methods = order_cycle_shared_shipping_methods(@order_cycle)
= form_for [main_app, :admin, @order_cycle], html: { class: "order_cycle" } do |f|
= render 'wizard_progress'
%fieldset.no-border-bottom
%legend{ align: 'center'}= t('.checkout_options')
%table.checkout-options
%thead
%tr
%th= t('.distributor')
%th= t('.shipping_methods')
%th= t('.payment_methods')
- @order_cycle.distributors.each do |distributor|
- payment_methods = @order_cycle.attachable_payment_methods.where("distributor_id = ?", distributor.id).reject { |payment_method| shared_payment_methods.include?(payment_method) }
- shipping_methods = @order_cycle.attachable_shipping_methods.where("distributor_id = ?", distributor.id).reject { |shipping_method| shared_shipping_methods.include?(shipping_method) }
%tr
%td= distributor.name
%td
- shipping_methods.each do |shipping_method|
%p
%label
= check_box_tag "order_cycle[preferred_shipping_method_ids][]",
shipping_method.id, @order_cycle.shipping_methods.include?(shipping_method),
id: "order_cycle_preferred_shipping_method_ids_#{shipping_method.id}"
= shipping_method.name
- distributor.shipping_methods.backend.each do |shipping_method|
%label.disabled
= check_box_tag nil, nil, false, disabled: true
= shipping_method.name
= "(#{t('.back_end')})"
- if shipping_methods.none? && distributor.shipping_methods.backend.none?
%p.text-center
= t('.no_shipping_methods')
%td
- if payment_methods.any?
%ul
- payment_methods.each do |payment_method|
%li= payment_method.name
- else
%p.text-center
= t('.no_payment_methods')
- if shared_payment_methods.any? || shared_shipping_methods.any?
%tr
%td= t('.shared')
%td
- if shared_shipping_methods.any?
= f.collection_check_boxes :shipping_method_ids, shared_shipping_methods, :id, :name do |input|
- shared_shipping_method = input.object
%p
= input.check_box
= input.label
%p
= "&mdash;<em>#{shared_shipping_method.distributors.where(id: @order_cycle.distributor_ids).map(&:name).join(", ")}</em>".html_safe
%td
- if shared_payment_methods.any?
%ul
- shared_payment_methods.each do |shared_payment_method|
%li
= shared_payment_method.name
%p
= "&mdash;<em>#{shared_payment_method.distributors.where(id: @order_cycle.distributor_ids).map(&:name).join(", ")}</em>".html_safe
%div#save-bar
%div.container
%div.seven.columns.alpha
- if @order_cycle.errors.any?
%h5#status-message.error
= @order_cycle.errors.to_a.to_sentence
%div.nine.columns.omega.text-right
= hidden_field_tag :context, :checkout_options
= f.submit t('.save'), class: "red", name: :save
= f.submit t('.save_and_back_to_list'), class: "red", name: :save_and_back_to_list
%a.button.cancel{ href: main_app.admin_order_cycles_path }
= t('.cancel')

View File

@@ -10,7 +10,8 @@
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%input.red{ type: "button", value: t('.save'), ng: { click: "submit($event, null)", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input.red{ type: "button", value: t('.save_and_back_to_list'), ng: { click: "submit($event, '#{main_app.admin_order_cycles_path}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input.red{ type: "button", value: t('.save_and_next'), ng: { click: "submit($event, '#{main_app.admin_order_cycle_checkout_options_path(@order_cycle)}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input{ type: "button", value: t('.next'), ng: { click: "cancel('#{main_app.admin_order_cycle_checkout_options_path(@order_cycle)}')", disabled: "order_cycle_form.$dirty" } }
%input{ type: "button", ng: { value: "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", click: "cancel('#{main_app.admin_order_cycles_path}')" } }
%fieldset.no-border-bottom

View File

@@ -11,6 +11,10 @@
h5 {
color: $spree-blue;
&.error {
color: $red-500;
}
}
input {

View File

@@ -96,6 +96,14 @@ form.order_cycle {
.icon-question-sign {
font-size: 18px;
}
table.checkout-options {
ul {
margin-left: 1em;
}
p, li {
margin: 0.5em 0;
}
}
table.exchanges {
tr td.active {
width: 20px;

View File

@@ -1173,15 +1173,29 @@ en:
tags: "Tags"
delivery_details: "Delivery Details"
fees: "Fees"
next: "Next"
previous: "Previous"
save: "Save"
save_and_back_to_list: "Save and Back to List"
save_and_next: "Save and Next"
cancel: "Cancel"
back_to_list: "Back To List"
checkout_options:
back_end: "Back office only"
cancel: "Cancel"
checkout_options: "Checkout options"
distributor: "Distributor"
no_payment_methods: Each distributor on this order cycle requires at least one payment method.
no_shipping_methods: Each distributor on this order cycle requires at least one shipping method.
payment_methods: "Payment Methods"
save: "Save"
save_and_back_to_list: "Save and Back to List"
shared: "Shared"
shipping_methods: "Shipping Methods"
wizard_progress:
edit: "1. General Settings"
incoming: "2. Incoming Products"
outgoing: "3. Outgoing Products"
checkout_options: "4. Checkout Options"
exchange_form:
pickup_time_tip: When orders from this OC will be ready for the customer
pickup_instructions_placeholder: "Pick-up instructions"

View File

@@ -15,6 +15,7 @@ Openfoodnetwork::Application.routes.draw do
post :bulk_update, on: :collection, as: :bulk_update
get :incoming
get :outgoing
get :checkout_options
member do
get :clone

View File

@@ -677,6 +677,24 @@ describe OrderCycle do
end
end
describe "#attachable_shipping_methods" do
it "includes shipping methods from the distributors on the order cycle" do
shipping_method = create(:shipping_method)
enterprise = create(:enterprise, shipping_methods: [shipping_method])
oc = create(:simple_order_cycle, distributors: [enterprise])
expect(oc.attachable_shipping_methods).to eq([shipping_method])
end
it "does not include backoffice only shipping methods" do
shipping_method = create(:shipping_method, display_on: "back_end")
enterprise = create(:enterprise, shipping_methods: [shipping_method])
oc = create(:simple_order_cycle, distributors: [enterprise])
expect(oc.attachable_shipping_methods).to be_empty
end
end
describe "#simple?" do
it "returns true if the coordinator sells their own products i.e. shops" do
order_cycle = build(:simple_order_cycle, coordinator: build(:enterprise, sells: "own"))

View File

@@ -0,0 +1,269 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderAvailableShippingMethods do
context "when the order has no current_distributor" do
it "returns an empty array" do
order_cycle = create(:sells_own_order_cycle)
order = build(:order, distributor: nil, order_cycle: order_cycle)
expect(OrderAvailableShippingMethods.new(order).to_a).to eq []
end
end
it "does not return 'back office only' shipping method" do
distributor = create(:distributor_enterprise)
frontend_shipping_method = create(:shipping_method, distributors: [distributor])
backoffice_only_shipping_method = create(:shipping_method,
distributors: [distributor], display_on: 'back_end')
order_cycle = create(:sells_own_order_cycle)
order = build(:order, distributor: distributor, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).to eq [frontend_shipping_method]
end
context "when no tag rules are in effect" do
context "order cycle selling own produce only i.e. shipping methods cannot be customised" do
it "returns all shipping methods belonging to the enterprise" do
order_cycle = create(:sells_own_order_cycle)
enterprise = order_cycle.coordinator
shipping_method = create(:shipping_method, distributors: [enterprise])
other_enterprise = create(:enterprise)
other_enterprise_shipping_method = create(:shipping_method,
distributors: [other_enterprise])
order = build(:order, distributor: enterprise, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(order_cycle.shipping_methods).to be_empty
expect(available_shipping_methods).to eq [shipping_method]
end
end
context "distributor order cycle" do
it "only returns shipping methods which belong to the order distributor
and have been added to the order cycle" do
distributor = create(:distributor_enterprise)
shipping_method_i = create(:shipping_method, distributors: [distributor])
shipping_method_ii = create(:shipping_method, distributors: [distributor])
order_cycle = create(:simple_order_cycle,
distributors: [distributor], shipping_methods: [shipping_method_i])
order = build(:order, distributor: distributor, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).to eq order_cycle.shipping_methods
expect(available_shipping_methods).to eq [shipping_method_i]
end
it "doesn't return shipping methods which have been added to the order cycle
when they don't belong to the order distributor" do
distributor_i = create(:distributor_enterprise)
distributor_ii = create(:distributor_enterprise)
shipping_method_i = create(:shipping_method, distributors: [distributor_i])
shipping_method_ii = create(:shipping_method, distributors: [distributor_ii])
order_cycle = create(:simple_order_cycle,
distributors: [distributor_i, distributor_ii],
shipping_methods: [shipping_method_i, shipping_method_ii])
order = build(:order, distributor: distributor_ii, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).not_to eq order_cycle.shipping_methods
expect(available_shipping_methods).to eq [shipping_method_ii]
end
end
end
context "when FilterShippingMethods tag rules are in effect" do
let(:user) { create(:user) }
let(:distributor) { create(:distributor_enterprise) }
let(:other_distributor) { create(:distributor_enterprise) }
let!(:distributor_shipping_method) { create(:shipping_method, distributors: [distributor]) }
let!(:other_distributor_shipping_method) do
create(:shipping_method, distributors: [other_distributor])
end
let(:customer) { create(:customer, user: user, enterprise: distributor) }
let!(:tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
preferred_customer_tags: "local",
preferred_shipping_method_tags: "local-delivery")
}
let!(:default_tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
is_default: true,
preferred_shipping_method_tags: "local-delivery")
}
let!(:tagged_sm) { distributor_shipping_method }
let!(:untagged_sm) { other_distributor_shipping_method }
before do
tagged_sm.update_attribute(:tag_list, 'local-delivery')
distributor.shipping_methods = [tagged_sm, untagged_sm]
end
context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'hidden')
}
context "order cycle selling own produce only" do
let(:order_cycle) { create(:sells_own_order_cycle) }
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
context "when the customer is nil" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order).to_a }
it "applies default action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
context "when a customer is present" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "and the customer's tags match" do
before do
customer.update_attribute(:tag_list, 'local')
end
it "applies the action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "and the customer's tags don't match" do
before do
customer.update_attribute(:tag_list, 'something')
end
it "applies the default action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
end
end
context "distributor order cycle" do
context "when the shipping method without the tag rule is attached to the order cycle
and the shipping method with the tag rule is not" do
let(:order_cycle) do
create(:distributor_order_cycle,
distributors: [distributor], shipping_methods: [untagged_sm])
end
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "when the customer's tags match" do
before do
customer.update_attribute(:tag_list, 'local')
end
it "doesn't display the shipping method with the prefered visibility 'visible' tag
even though the customer's tags match
because it hasn't been attached to the order cycle" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
end
end
end
context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'visible')
}
context "order cycle selling own produce only" do
let(:order_cycle) { create(:sells_own_order_cycle) }
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
context "when the customer is nil" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order).to_a }
it "applies default action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "when a customer is present" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "and the customer's tags match" do
before do
customer.update_attribute(:tag_list, 'local')
end
it "applies the action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
context "and the customer's tags don't match" do
before do
customer.update_attribute(:tag_list, 'something')
end
it "applies the default action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
end
end
context "distributor order cycle" do
context "when the shipping method without the tag rule is attached to the order cycle
and the shipping method with the tag rule is not" do
let(:order_cycle) do
create(:distributor_order_cycle,
distributors: [distributor], shipping_methods: [untagged_sm])
end
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
context "when the customer is nil" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order).to_a }
it "doesn't display the shipping method tagged to be visible by default
because it is not attached to the order cycle" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
context "when a customer is present" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "when the customer's tags don't match" do
before do
customer.update_attribute(:tag_list, 'something')
end
it "doesn't display the shipping method tagged to be visible by default
because it is not attached to the order cycle" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
end
end
end
end
end
end

View File

@@ -22,6 +22,8 @@ describe '
v2 = create(:variant, product: product)
distributor = create(:distributor_enterprise, name: 'My distributor',
with_payment_and_shipping: true)
shipping_method_i = distributor.shipping_methods.first
shipping_method_ii = create(:shipping_method, distributors: [distributor])
# Relationships required for interface to work
create(:enterprise_relationship, parent: supplier, child: coordinator,
@@ -129,6 +131,12 @@ describe '
select 'Distributor fee',
from: 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_fee_id'
click_button 'Save and Next'
# And I select preferred shipping methods
check "order_cycle_preferred_shipping_method_ids_#{shipping_method_i.id}"
uncheck "order_cycle_preferred_shipping_method_ids_#{shipping_method_ii.id}"
click_button 'Save and Back to List'
oc = OrderCycle.last
@@ -161,5 +169,8 @@ describe '
expect(exchange.pickup_time).to eq('pickup time')
expect(exchange.pickup_instructions).to eq('pickup instructions')
expect(exchange.tag_list).to eq(['wholesale'])
# And the shipping method should be attached
expect(oc.shipping_methods).to eq([shipping_method_i])
end
end